Routing Forms (#2785)
* Add Routing logic to Query builder * Make a working redirect * Make it an app * Move pages and components to App * Integrate all pages in the app * Integrate prisma everywhere * Fix Routing Link * Add routing preview * Fixes * Get deplouyed on preview with ts disabled * Fix case * add reordering for routes * Move away from react DnD * Add sidebar * Add sidebar support and select support * Various fixes and improvements * Ignore eslint temporarly * Route might be falsy * Make CalNumber support required validation * Loader improvements * Add SSR support * Fix few typescript issues * More typesafety, download csv, bug fiees * Add seo friendly link * Avoid seding credebtials to frontend * Self review fixes * Improvements in app-store * Cahnge Form layout * Add scaffolding for app tests * Add playwright tests and add user check in serving data * Add CI tests * Add route builder test * Styling * Apply suggestions from code review Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> * Changes as per loom feedback * Increase time for tests * Fix PR suggestions * Import CSS only in the module * Fix codacy issues * Move the codebbase to ee and add PRO and license check * Add Badge * Avoid lodash import * Fix TS error * Fix lint errors * Fix bug to merge conflicts resolution - me query shouldnt cause the Shell to go in loading state Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com>
This commit is contained in:
parent
7ec5f01647
commit
58d1c28e9d
|
@ -0,0 +1,99 @@
|
||||||
|
name: E2E App-Store Apps
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ feature/event-routing ]
|
||||||
|
pull_request_target: # So we can test on forks
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- apps/api/**
|
||||||
|
- apps/console/**
|
||||||
|
- apps/docs/**
|
||||||
|
- apps/swagger/**
|
||||||
|
- apps/website/**
|
||||||
|
- apps/web/public/**
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 20
|
||||||
|
name: E2E App-Store Apps
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: ["16.x"]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET: secret
|
||||||
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
|
GOOGLE_LOGIN_ENABLED: true
|
||||||
|
# CRON_API_KEY: xxx
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||||
|
PAYMENT_FEE_FIXED: 10
|
||||||
|
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
SAML_ADMINS: pro@example.com
|
||||||
|
NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||||
|
NEXT_PUBLIC_IS_E2E: 1
|
||||||
|
# EMAIL_FROM: e2e@cal.com
|
||||||
|
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
|
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||||
|
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||||
|
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||||
|
# MS_GRAPH_CLIENT_ID: xxx
|
||||||
|
# MS_GRAPH_CLIENT_SECRET: xxx
|
||||||
|
# ZOOM_CLIENT_ID: xxx
|
||||||
|
# ZOOM_CLIENT_SECRET: xxx
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.1
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: calendso
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
|
fetch-depth: 2
|
||||||
|
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||||
|
- name: Use Node ${{ matrix.node }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: playwright-cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Caches/ms-playwright
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
${{ github.workspace }}/node_modules/playwright
|
||||||
|
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: cache-playwright-
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- name: Install playwright deps
|
||||||
|
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Run Tests
|
||||||
|
run: yarn app-e2e-quick
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
if: ${{ always() }}
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: test-results-core
|
||||||
|
path: packages/app-store/**/playwright/results
|
|
@ -22,10 +22,6 @@
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
|
||||||
// Try start the task on folder open
|
|
||||||
"runOptions": {
|
|
||||||
"runOn": "folderOpen"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,15 +12,20 @@ import { ChevronLeftIcon } from "@heroicons/react/solid";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, SkeletonButton } from "@calcom/ui";
|
import { Button, SkeletonButton } from "@calcom/ui";
|
||||||
|
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import Shell from "@components/Shell";
|
import Shell from "@components/Shell";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
export default function App({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
logo,
|
logo,
|
||||||
|
@ -36,25 +41,19 @@ export default function App({
|
||||||
email,
|
email,
|
||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
}: {
|
isProOnly,
|
||||||
name: string;
|
}: Parameters<typeof App>[0]) => {
|
||||||
type: AppType["type"];
|
|
||||||
isGlobal?: AppType["isGlobal"];
|
|
||||||
logo: string;
|
|
||||||
body: React.ReactNode;
|
|
||||||
categories: string[];
|
|
||||||
author: string;
|
|
||||||
pro?: boolean;
|
|
||||||
price?: number;
|
|
||||||
commission?: number;
|
|
||||||
feeType?: AppType["feeType"];
|
|
||||||
docs?: string;
|
|
||||||
website?: string;
|
|
||||||
email: string; // required
|
|
||||||
tos?: string;
|
|
||||||
privacy?: string;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||||
|
|
||||||
|
const mutation = useAddAppMutation(null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast("App successfully installed", "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error instanceof Error) showToast(error.message || "App could not be installed", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const priceInDollar = Intl.NumberFormat("en-US", {
|
const priceInDollar = Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
|
@ -90,9 +89,8 @@ export default function App({
|
||||||
getInstalledApp(type);
|
getInstalledApp(type);
|
||||||
}, [type]);
|
}, [type]);
|
||||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Shell large isPublic>
|
|
||||||
<div className="-mx-4 md:-mx-8">
|
<div className="-mx-4 md:-mx-8">
|
||||||
<div className="bg-gray-50 px-8">
|
<div className="bg-gray-50 px-8">
|
||||||
<Link href="/apps">
|
<Link href="/apps">
|
||||||
|
@ -104,7 +102,14 @@ export default function App({
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
|
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
|
||||||
<header className="px-4 py-2">
|
<header className="px-4 py-2">
|
||||||
|
<div className="flex items-center">
|
||||||
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
||||||
|
{isProOnly && user?.plan === "FREE" ? (
|
||||||
|
<Badge className="ml-2" variant="default">
|
||||||
|
PRO
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<h2 className="text-sm text-gray-500">
|
<h2 className="text-sm text-gray-500">
|
||||||
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -122,21 +127,43 @@ export default function App({
|
||||||
</Button>
|
</Button>
|
||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={type}
|
type={type}
|
||||||
render={(buttonProps) => (
|
isProOnly={isProOnly}
|
||||||
<Button StartIcon={PlusIcon} data-testid="install-app-button" {...buttonProps}>
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
|
if (useDefaultComponent) {
|
||||||
|
props = {
|
||||||
|
onClick: () => {
|
||||||
|
mutation.mutate({ type });
|
||||||
|
},
|
||||||
|
loading: mutation.isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button StartIcon={PlusIcon} {...props} data-testid="install-app-button">
|
||||||
{t("add_another")}
|
{t("add_another")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={type}
|
type={type}
|
||||||
render={(buttonProps) => (
|
isProOnly={isProOnly}
|
||||||
<Button data-testid="install-app-button" {...buttonProps}>
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
|
if (useDefaultComponent) {
|
||||||
|
props = {
|
||||||
|
onClick: () => {
|
||||||
|
mutation.mutate({ type });
|
||||||
|
},
|
||||||
|
loading: mutation.isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button data-testid="install-app-button" {...props}>
|
||||||
{t("install_app")}
|
{t("install_app")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -144,9 +171,7 @@ export default function App({
|
||||||
)}
|
)}
|
||||||
{price !== 0 && (
|
{price !== 0 && (
|
||||||
<small className="block text-right">
|
<small className="block text-right">
|
||||||
{feeType === "usage-based"
|
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
||||||
? commission + "% + " + priceInDollar + "/booking"
|
|
||||||
: priceInDollar}
|
|
||||||
{feeType === "monthly" && "/" + t("month")}
|
{feeType === "monthly" && "/" + t("month")}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
@ -259,7 +284,38 @@ export default function App({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App(props: {
|
||||||
|
name: string;
|
||||||
|
type: AppType["type"];
|
||||||
|
isGlobal?: AppType["isGlobal"];
|
||||||
|
logo: string;
|
||||||
|
body: React.ReactNode;
|
||||||
|
categories: string[];
|
||||||
|
author: string;
|
||||||
|
pro?: boolean;
|
||||||
|
price?: number;
|
||||||
|
commission?: number;
|
||||||
|
feeType?: AppType["feeType"];
|
||||||
|
docs?: string;
|
||||||
|
website?: string;
|
||||||
|
email: string; // required
|
||||||
|
tos?: string;
|
||||||
|
privacy?: string;
|
||||||
|
licenseRequired: AppType["licenseRequired"];
|
||||||
|
isProOnly: AppType["isProOnly"];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Shell large isPublic>
|
||||||
|
{props.licenseRequired ? (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...props} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...props} />
|
||||||
|
)}
|
||||||
</Shell>
|
</Shell>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export interface NavTabProps {
|
||||||
tabName?: string;
|
tabName?: string;
|
||||||
icon?: SVGComponent;
|
icon?: SVGComponent;
|
||||||
adminRequired?: boolean;
|
adminRequired?: boolean;
|
||||||
|
className?: string;
|
||||||
}[];
|
}[];
|
||||||
linkProps?: Omit<LinkProps, "href">;
|
linkProps?: Omit<LinkProps, "href">;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||||
: noop;
|
: noop;
|
||||||
|
|
||||||
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
||||||
|
const className = tab.className || "";
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
<Link key={tab.name} href={href} {...linkProps}>
|
<Link key={tab.name} href={href} {...linkProps}>
|
||||||
|
@ -68,7 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-neutral-900 text-neutral-900"
|
? "border-neutral-900 text-neutral-900"
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
|
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
aria-current={isCurrent ? "page" : undefined}>
|
aria-current={isCurrent ? "page" : undefined}>
|
||||||
{tab.icon && (
|
{tab.icon && (
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
// This component is abstracted from /event-types/[type] for common usecase.
|
||||||
|
import { PencilIcon } from "@heroicons/react/solid";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function PencilEdit({
|
||||||
|
value,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onChange = () => {},
|
||||||
|
placeholder = "",
|
||||||
|
readOnly = false,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}) {
|
||||||
|
const [editIcon, setEditIcon] = useState(true);
|
||||||
|
const onDivClick = !readOnly
|
||||||
|
? () => {
|
||||||
|
return setEditIcon(false);
|
||||||
|
}
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
() => {};
|
||||||
|
return (
|
||||||
|
<div className="group relative min-h-[28px] cursor-pointer" onClick={onDivClick}>
|
||||||
|
{editIcon ? (
|
||||||
|
<>
|
||||||
|
<h1
|
||||||
|
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
|
||||||
|
className="inline-block pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
|
||||||
|
{value}
|
||||||
|
</h1>
|
||||||
|
{!readOnly ? (
|
||||||
|
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div style={{ marginBottom: -11 }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
style={{ top: -6, fontSize: 22 }}
|
||||||
|
required
|
||||||
|
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultValue={value}
|
||||||
|
onBlur={(e) => {
|
||||||
|
setEditIcon(true);
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { SelectorIcon } from "@heroicons/react/outline";
|
import { SelectorIcon } from "@heroicons/react/outline";
|
||||||
|
import { CollectionIcon } from "@heroicons/react/solid";
|
||||||
import {
|
import {
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
|
@ -125,6 +126,12 @@ const Layout = ({
|
||||||
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
|
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: routingForms } = trpc.useQuery([
|
||||||
|
"viewer.appById",
|
||||||
|
{
|
||||||
|
appId: "routing_forms",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const navigation = [
|
const navigation = [
|
||||||
|
@ -146,6 +153,14 @@ const Layout = ({
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
current: router.asPath.startsWith("/availability"),
|
current: router.asPath.startsWith("/availability"),
|
||||||
},
|
},
|
||||||
|
routingForms
|
||||||
|
? {
|
||||||
|
name: "Routing Forms",
|
||||||
|
href: "/apps/routing_forms/forms",
|
||||||
|
icon: CollectionIcon,
|
||||||
|
current: router.asPath.startsWith("/apps/routing_forms/"),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
{
|
{
|
||||||
name: t("workflows"),
|
name: t("workflows"),
|
||||||
href: "/workflows",
|
href: "/workflows",
|
||||||
|
@ -157,7 +172,7 @@ const Layout = ({
|
||||||
name: t("apps"),
|
name: t("apps"),
|
||||||
href: "/apps",
|
href: "/apps",
|
||||||
icon: ViewGridIcon,
|
icon: ViewGridIcon,
|
||||||
current: router.asPath.startsWith("/apps"),
|
current: router.asPath.startsWith("/apps") && !router.asPath.startsWith("/apps/routing_forms/"),
|
||||||
child: [
|
child: [
|
||||||
{
|
{
|
||||||
name: t("app_store"),
|
name: t("app_store"),
|
||||||
|
@ -212,7 +227,6 @@ const Layout = ({
|
||||||
<KBarTrigger />
|
<KBarTrigger />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* logo icon for tablet */}
|
{/* logo icon for tablet */}
|
||||||
<Link href="/event-types">
|
<Link href="/event-types">
|
||||||
<a className="text-center md:inline lg:hidden">
|
<a className="text-center md:inline lg:hidden">
|
||||||
|
@ -220,7 +234,8 @@ const Layout = ({
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
|
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
|
||||||
{navigation.map((item) => (
|
{navigation.map((item) =>
|
||||||
|
!item ? null : (
|
||||||
<Fragment key={item.name}>
|
<Fragment key={item.name}>
|
||||||
<Link href={item.href}>
|
<Link href={item.href}>
|
||||||
<a
|
<a
|
||||||
|
@ -266,7 +281,8 @@ const Layout = ({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<span className="group flex items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
<span className="group flex items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
||||||
<KBarTrigger />
|
<KBarTrigger />
|
||||||
</span>
|
</span>
|
||||||
|
@ -316,7 +332,6 @@ const Layout = ({
|
||||||
<span className="group flex items-center rounded-full p-2.5 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
<span className="group flex items-center rounded-full p-2.5 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
|
||||||
<KBarTrigger />
|
<KBarTrigger />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||||
<span className="sr-only">{t("settings")}</span>
|
<span className="sr-only">{t("settings")}</span>
|
||||||
<Link href="/settings/profile">
|
<Link href="/settings/profile">
|
||||||
|
@ -350,7 +365,7 @@ const Layout = ({
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
|
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
|
||||||
"block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8"
|
"block justify-between px-4 sm:flex sm:px-6 md:px-8"
|
||||||
)}>
|
)}>
|
||||||
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
||||||
<div className="mb-8 w-full">
|
<div className="mb-8 w-full">
|
||||||
|
@ -364,9 +379,7 @@ const Layout = ({
|
||||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
||||||
{props.heading}
|
{props.heading}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">
|
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
|
||||||
{props.subtitle}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -386,8 +399,11 @@ const Layout = ({
|
||||||
style={isEmbed ? { display: "none" } : {}}
|
style={isEmbed ? { display: "none" } : {}}
|
||||||
className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
|
className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
|
||||||
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
|
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
|
||||||
{navigation.flatMap((item, itemIdx) =>
|
{navigation.flatMap((item, itemIdx) => {
|
||||||
item.href === "/settings/profile" ? (
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item.href === "/settings/profile" ? (
|
||||||
[]
|
[]
|
||||||
) : (
|
) : (
|
||||||
<Link key={item.name} href={item.href}>
|
<Link key={item.name} href={item.href}>
|
||||||
|
@ -406,11 +422,11 @@ const Layout = ({
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{item.name}</span>
|
<span className="block truncate">{item.name}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
);
|
||||||
)}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
{/* add padding to content for mobile navigation*/}
|
{/* add padding to content for mobile navigation*/}
|
||||||
|
@ -453,7 +469,7 @@ export default function Shell(props: LayoutProps) {
|
||||||
const i18n = useViewerI18n();
|
const i18n = useViewerI18n();
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|
||||||
const isLoading = query.status === "loading" || isRedirectingToOnboarding || loading || !isReady;
|
const isLoading = isRedirectingToOnboarding || loading || !isReady;
|
||||||
|
|
||||||
// Don't show any content till translations are loaded.
|
// Don't show any content till translations are loaded.
|
||||||
// As they are cached infintely, this status would be loading just once for the app's lifetime until refresh
|
// As they are cached infintely, this status would be loading just once for the app's lifetime until refresh
|
||||||
|
@ -490,7 +506,9 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const [helpOpen, setHelpOpen] = useState(false);
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const onHelpItemSelect = () => {
|
const onHelpItemSelect = () => {
|
||||||
setHelpOpen(false);
|
setHelpOpen(false);
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
|
@ -514,14 +532,14 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
src={WEBAPP_URL + "/" + user?.username + "/avatar.png"}
|
src={WEBAPP_URL + "/" + user.username + "/avatar.png"}
|
||||||
alt={user?.username || "Nameless User"}
|
alt={user.username || "Nameless User"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!user?.away && (
|
{!user.away && (
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" />
|
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" />
|
||||||
)}
|
)}
|
||||||
{user?.away && (
|
{user.away && (
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" />
|
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -529,10 +547,10 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
<span className="flex flex-grow items-center truncate">
|
<span className="flex flex-grow items-center truncate">
|
||||||
<span className="flex-grow truncate text-sm">
|
<span className="flex-grow truncate text-sm">
|
||||||
<span className="block truncate font-medium text-gray-900">
|
<span className="block truncate font-medium text-gray-900">
|
||||||
{user?.name || "Nameless User"}
|
{user.name || "Nameless User"}
|
||||||
</span>
|
</span>
|
||||||
<span className="block truncate font-normal text-neutral-500">
|
<span className="block truncate font-normal text-neutral-500">
|
||||||
{user?.username
|
{user.username
|
||||||
? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
|
? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
|
||||||
? `cal.com/${user.username}`
|
? `cal.com/${user.username}`
|
||||||
: `/${user.username}`
|
: `/${user.username}`
|
||||||
|
@ -555,24 +573,24 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutation.mutate({ away: !user?.away });
|
mutation.mutate({ away: user?.away });
|
||||||
utils.invalidateQueries("viewer.me");
|
utils.invalidateQueries("viewer.me");
|
||||||
}}
|
}}
|
||||||
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
|
||||||
<MoonIcon
|
<MoonIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
user?.away
|
user.away
|
||||||
? "text-purple-500 group-hover:text-purple-700"
|
? "text-purple-500 group-hover:text-purple-700"
|
||||||
: "text-gray-500 group-hover:text-gray-700",
|
: "text-gray-500 group-hover:text-gray-700",
|
||||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{user?.away ? t("set_as_free") : t("set_as_away")}
|
{user.away ? t("set_as_free") : t("set_as_away")}
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
{user?.username && (
|
{user.username && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
|
||||||
logo={app.logo}
|
logo={app.logo}
|
||||||
rating={app.rating}
|
rating={app.rating}
|
||||||
reviews={app.reviews}
|
reviews={app.reviews}
|
||||||
|
isProOnly={app.isProOnly}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,10 @@ import Link from "next/link";
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
interface AppCardProps {
|
interface AppCardProps {
|
||||||
logo: string;
|
logo: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -10,9 +14,11 @@ interface AppCardProps {
|
||||||
description: string;
|
description: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
reviews?: number;
|
reviews?: number;
|
||||||
|
isProOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AppCard(props: AppCardProps) {
|
export default function AppCard(props: AppCardProps) {
|
||||||
|
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||||
return (
|
return (
|
||||||
<Link href={"/apps/" + props.slug}>
|
<Link href={"/apps/" + props.slug}>
|
||||||
<a
|
<a
|
||||||
|
@ -32,7 +38,14 @@ export default function AppCard(props: AppCardProps) {
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
<h3 className="font-medium">{props.name}</h3>
|
<h3 className="font-medium">{props.name}</h3>
|
||||||
|
{props.isProOnly && user?.plan === "FREE" ? (
|
||||||
|
<Badge className="ml-2" variant="default">
|
||||||
|
PRO
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
||||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||||
|
|
|
@ -30,6 +30,7 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||||
logo={app.logo}
|
logo={app.logo}
|
||||||
rating={app.rating}
|
rating={app.rating}
|
||||||
reviews={app.reviews}
|
reviews={app.reviews}
|
||||||
|
isProOnly={app.isProOnly}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import ReactSelect, { components, GroupBase, Props, InputProps } from "react-select";
|
import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
@ -61,4 +61,71 @@ function Select<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SelectWithValidation<
|
||||||
|
Option extends { label: string; value: string },
|
||||||
|
isMulti extends boolean = false,
|
||||||
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
|
>({
|
||||||
|
required = false,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
...remainingProps
|
||||||
|
}: SelectProps<Option, isMulti, Group> & { required?: boolean }) {
|
||||||
|
const [hiddenInputValue, _setHiddenInputValue] = useState(() => {
|
||||||
|
if (value instanceof Array || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return value.value || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
const setHiddenInputValue = useCallback((value: MultiValue<Option> | SingleValue<Option>) => {
|
||||||
|
let hiddenInputValue = "";
|
||||||
|
if (value instanceof Array) {
|
||||||
|
hiddenInputValue = value.map((val) => val.value).join(",");
|
||||||
|
} else {
|
||||||
|
hiddenInputValue = value?.value || "";
|
||||||
|
}
|
||||||
|
_setHiddenInputValue(hiddenInputValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHiddenInputValue(value);
|
||||||
|
}, [value, setHiddenInputValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("relative", remainingProps.className)}>
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
{...remainingProps}
|
||||||
|
onChange={(value, ...remainingArgs) => {
|
||||||
|
setHiddenInputValue(value);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(value, ...remainingArgs);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{required && (
|
||||||
|
<input
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
style={{
|
||||||
|
opacity: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: 1,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
value={hiddenInputValue}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onChange={() => {}}
|
||||||
|
// TODO:Not able to get focus to work
|
||||||
|
// onFocus={() => selectRef.current?.focus()}
|
||||||
|
required={required}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
export default Select;
|
export default Select;
|
||||||
|
|
|
@ -95,6 +95,11 @@ const nextConfig = {
|
||||||
source: "/team/:teamname/avatar.png",
|
source: "/team/:teamname/avatar.png",
|
||||||
destination: "/api/user/avatar?teamname=:teamname",
|
destination: "/api/user/avatar?teamname=:teamname",
|
||||||
},
|
},
|
||||||
|
// TODO: We can expose these rewrites in packages/app-store/*.generated.ts
|
||||||
|
{
|
||||||
|
source: "/forms/:formId",
|
||||||
|
destination: "/apps/routing_forms/routing-link/:formId",
|
||||||
|
},
|
||||||
/* TODO: have these files being served from another deployment or CDN {
|
/* TODO: have these files being served from another deployment or CDN {
|
||||||
source: "/embed/embed.js",
|
source: "/embed/embed.js",
|
||||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
"@radix-ui/react-radio-group": "^0.1.1",
|
"@radix-ui/react-radio-group": "^0.1.1",
|
||||||
"@radix-ui/react-slider": "^0.1.1",
|
"@radix-ui/react-slider": "^0.1.1",
|
||||||
"@radix-ui/react-switch": "^0.1.1",
|
"@radix-ui/react-switch": "^0.1.1",
|
||||||
|
"@radix-ui/react-toggle-group": "^0.1.5",
|
||||||
"@radix-ui/react-tooltip": "^0.1.0",
|
"@radix-ui/react-tooltip": "^0.1.0",
|
||||||
"@stripe/react-stripe-js": "^1.8.0",
|
"@stripe/react-stripe-js": "^1.8.0",
|
||||||
"@stripe/stripe-js": "^1.29.0",
|
"@stripe/stripe-js": "^1.29.0",
|
||||||
|
|
|
@ -14,19 +14,28 @@ import path from "path";
|
||||||
* This will allow us to keep all app-specific static assets in the same directory.
|
* This will allow us to keep all app-specific static assets in the same directory.
|
||||||
*/
|
*/
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const [appName, fileName] = Array.isArray(req.query.static) ? req.query.static : [req.query.static];
|
const queryParts = Array.isArray(req.query.static) ? req.query.static : [req.query.static];
|
||||||
|
let appPath, fileName;
|
||||||
|
if (queryParts[0] === "ee") {
|
||||||
|
const appName = queryParts[1];
|
||||||
|
if (!appName) {
|
||||||
|
return res.status(400).json({ error: true, message: "No app name provided" });
|
||||||
|
}
|
||||||
|
appPath = path.join("ee", appName);
|
||||||
|
fileName = queryParts[2];
|
||||||
|
} else {
|
||||||
|
[appPath, fileName] = queryParts;
|
||||||
|
}
|
||||||
|
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
return res.status(400).json({ error: true, message: "No file name provided" });
|
return res.status(400).json({ error: true, message: "No file name provided" });
|
||||||
}
|
}
|
||||||
if (!appName) {
|
if (!appPath) {
|
||||||
return res.status(400).json({ error: true, message: "No app name provided" });
|
return res.status(400).json({ error: true, message: "No app name provided" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileNameParts = fileName.split(".");
|
const fileNameParts = fileName.split(".");
|
||||||
const { [fileNameParts.length - 1]: fileExtension } = fileNameParts;
|
const { [fileNameParts.length - 1]: fileExtension } = fileNameParts;
|
||||||
const STATIC_PATH = path.join(process.cwd(), "..", "..", "packages/app-store", appName, "static", fileName);
|
const STATIC_PATH = path.join(process.cwd(), "..", "..", "packages/app-store", appPath, "static", fileName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageBuffer = fs.readFileSync(STATIC_PATH);
|
const imageBuffer = fs.readFileSync(STATIC_PATH);
|
||||||
const mimeType = mime.lookup(fileExtension);
|
const mimeType = mime.lookup(fileExtension);
|
||||||
|
|
|
@ -1,10 +1,43 @@
|
||||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
const defaultIntegrationAddHandler = async ({
|
||||||
|
slug,
|
||||||
|
supportsMultipleInstalls,
|
||||||
|
appType,
|
||||||
|
user,
|
||||||
|
createCredential,
|
||||||
|
}: {
|
||||||
|
slug: string;
|
||||||
|
supportsMultipleInstalls: boolean;
|
||||||
|
appType: string;
|
||||||
|
user?: Session["user"];
|
||||||
|
createCredential: AppDeclarativeHandler["createCredential"];
|
||||||
|
}) => {
|
||||||
|
if (!user?.id) {
|
||||||
|
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||||
|
}
|
||||||
|
if (!supportsMultipleInstalls) {
|
||||||
|
const alreadyInstalled = await prisma.credential.findFirst({
|
||||||
|
where: {
|
||||||
|
appId: slug,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (alreadyInstalled) {
|
||||||
|
throw new Error("App is already installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await createCredential({ user: user, appType, slug });
|
||||||
|
};
|
||||||
|
|
||||||
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 getSession({ req });
|
req.session = await getSession({ req });
|
||||||
|
@ -22,18 +55,28 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
||||||
const handlerKey = deriveAppDictKeyFromType(appName, handlerMap);
|
const handlerKey = deriveAppDictKeyFromType(appName, handlerMap);
|
||||||
const handlers = await handlerMap[handlerKey as keyof typeof handlerMap];
|
const handlers = await handlerMap[handlerKey as keyof typeof handlerMap];
|
||||||
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
|
const handler = handlers[apiEndpoint as keyof typeof handlers] as AppHandler;
|
||||||
if (typeof handler !== "function")
|
let redirectUrl = "/apps/installed";
|
||||||
|
if (typeof handler === "undefined")
|
||||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||||
|
|
||||||
|
if (typeof handler === "function") {
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
|
} else {
|
||||||
|
await defaultIntegrationAddHandler({ user: req.session?.user, ...handler });
|
||||||
|
redirectUrl = handler.redirectUrl;
|
||||||
|
res.json({ url: redirectUrl });
|
||||||
|
}
|
||||||
return res.status(200);
|
return res.status(200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
if (error instanceof HttpError) {
|
if (error instanceof HttpError) {
|
||||||
return res.status(error.statusCode).json({ message: error.message });
|
return res.status(error.statusCode).json({ message: error.message });
|
||||||
}
|
}
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return res.status(400).json({ message: error.message });
|
||||||
|
}
|
||||||
return res.status(404).json({ message: `API handler not found` });
|
return res.status(404).json({ message: `API handler not found` });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing_forms/pages/app-routing.config";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
|
||||||
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
|
// TODO: It is a candidate for apps.*.generated.*
|
||||||
|
const AppsRouting = {
|
||||||
|
routing_forms: RoutingFormsRoutingConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRoute(appName: string, pages: string[]) {
|
||||||
|
const routingConfig = AppsRouting[appName as keyof typeof AppsRouting];
|
||||||
|
type NotFound = {
|
||||||
|
notFound: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!routingConfig) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainPage = pages[0];
|
||||||
|
const appPage = routingConfig[mainPage as keyof typeof routingConfig];
|
||||||
|
type Found = {
|
||||||
|
notFound: false;
|
||||||
|
// A component than can accept any properties
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
Component: (props: any) => JSX.Element;
|
||||||
|
getServerSideProps: AppGetServerSideProps;
|
||||||
|
};
|
||||||
|
if (!appPage) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
} as NotFound;
|
||||||
|
}
|
||||||
|
return { notFound: false, Component: appPage.default, ...appPage } as Found;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppPage(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const appName = props.appName;
|
||||||
|
const router = useRouter();
|
||||||
|
const pages = router.query.pages as string[];
|
||||||
|
const route = getRoute(appName, pages);
|
||||||
|
|
||||||
|
const componentProps = {
|
||||||
|
...props,
|
||||||
|
pages: pages.slice(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!route || route.notFound) {
|
||||||
|
throw new Error("Route can't be undefined");
|
||||||
|
}
|
||||||
|
return <route.Component {...componentProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(
|
||||||
|
context: GetServerSidePropsContext<{
|
||||||
|
slug: string;
|
||||||
|
pages: string[];
|
||||||
|
appPages?: string[];
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const { params } = context;
|
||||||
|
if (!params) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const appName = params.slug;
|
||||||
|
const pages = params.pages;
|
||||||
|
const route = getRoute(appName, pages);
|
||||||
|
if (route.notFound) {
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
if (route.getServerSideProps) {
|
||||||
|
// TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
|
||||||
|
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
|
||||||
|
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
|
||||||
|
params.appPages = pages.slice(1);
|
||||||
|
const session = await getSession({ req: context.req });
|
||||||
|
const user = session?.user;
|
||||||
|
|
||||||
|
const result = await route.getServerSideProps(
|
||||||
|
context as GetServerSidePropsContext<{
|
||||||
|
slug: string;
|
||||||
|
pages: string[];
|
||||||
|
appPages: string[];
|
||||||
|
}>,
|
||||||
|
prisma,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
if (result.notFound) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (result.redirect) {
|
||||||
|
return {
|
||||||
|
redirect: result.redirect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
appName,
|
||||||
|
appUrl: `/apps/${appName}`,
|
||||||
|
...result.props,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
appName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,8 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
docs={data.docsUrl}
|
docs={data.docsUrl}
|
||||||
website={data.url}
|
website={data.url}
|
||||||
email={data.email}
|
email={data.email}
|
||||||
|
licenseRequired={data.licenseRequired}
|
||||||
|
isProOnly={data.isProOnly}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
// privacy="https://zoom.us/privacy"
|
// privacy="https://zoom.us/privacy"
|
||||||
body={<MDXRemote {...source} components={components} />}
|
body={<MDXRemote {...source} components={components} />}
|
||||||
|
|
|
@ -39,6 +39,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||||
description={app.description}
|
description={app.description}
|
||||||
logo={app.logo}
|
logo={app.logo}
|
||||||
rating={app.rating}
|
rating={app.rating}
|
||||||
|
isProOnly={app.isProOnly}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -77,12 +78,12 @@ export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||||
slug: true,
|
slug: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const appSlugs = appQuery.map((category) => category.slug);
|
|
||||||
|
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||||
|
|
||||||
const appStore = await getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
|
|
||||||
const apps = appStore.filter((app) => appSlugs.includes(app.slug));
|
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
apps,
|
apps,
|
||||||
|
|
|
@ -985,6 +985,7 @@
|
||||||
"no_active_event_types": "No active event types",
|
"no_active_event_types": "No active event types",
|
||||||
"new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}",
|
"new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}",
|
||||||
"new_seat_title": "Someone has added themselves to an event",
|
"new_seat_title": "Someone has added themselves to an event",
|
||||||
|
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
||||||
"invalid_number": "Invalid phone number",
|
"invalid_number": "Invalid phone number",
|
||||||
"navigate": "Navigate",
|
"navigate": "Navigate",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import _ from "lodash";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import app_RoutingForms from "@calcom/app-store/ee/routing_forms/trpc-router";
|
||||||
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
||||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
@ -650,6 +651,24 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.query("appById", {
|
||||||
|
input: z.object({
|
||||||
|
appId: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { user } = ctx;
|
||||||
|
const appId = input.appId;
|
||||||
|
const { credentials } = user;
|
||||||
|
const apps = getApps(credentials);
|
||||||
|
const appFromDb = apps.find((app) => app.credential?.appId === appId);
|
||||||
|
if (!appFromDb) {
|
||||||
|
return appFromDb;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { credential: _, credentials: _1, ...app } = appFromDb;
|
||||||
|
return app;
|
||||||
|
},
|
||||||
|
})
|
||||||
.query("web3Integration", {
|
.query("web3Integration", {
|
||||||
async resolve({ ctx }) {
|
async resolve({ ctx }) {
|
||||||
const { user } = ctx;
|
const { user } = ctx;
|
||||||
|
@ -1211,5 +1230,10 @@ export const viewerRouter = createRouter()
|
||||||
.merge("availability.", availabilityRouter)
|
.merge("availability.", availabilityRouter)
|
||||||
.merge("teams.", viewerTeamsRouter)
|
.merge("teams.", viewerTeamsRouter)
|
||||||
.merge("webhook.", webhookRouter)
|
.merge("webhook.", webhookRouter)
|
||||||
|
.merge("apiKeys.", apiKeysRouter)
|
||||||
|
.merge("slots.", slotsRouter)
|
||||||
.merge("workflows.", workflowsRouter)
|
.merge("workflows.", workflowsRouter)
|
||||||
.merge("apiKeys.", apiKeysRouter);
|
|
||||||
|
// NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
|
||||||
|
// After that there would just one merge call here for all the apps.
|
||||||
|
.merge("app_routing_forms.", app_RoutingForms);
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
/* Find a way to not require this - App files don't belong here. */
|
||||||
|
"../../packages/app-store/routing_forms/env.d.ts",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"../../packages/types/*.d.ts",
|
"../../packages/types/*.d.ts",
|
||||||
"../../packages/types/next-auth.d.ts",
|
"../../packages/types/next-auth.d.ts",
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"packages/embeds/*",
|
"packages/embeds/*",
|
||||||
"packages/app-store/*"
|
"packages/app-store/*",
|
||||||
|
"packages/app-store/ee/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build --scope=\"@calcom/web\" --include-dependencies",
|
"build": "turbo run build --scope=\"@calcom/web\" --include-dependencies",
|
||||||
|
@ -45,7 +46,8 @@
|
||||||
"type-check": "turbo run type-check",
|
"type-check": "turbo run type-check",
|
||||||
"app-store": "yarn workspace @calcom/app-store-cli cli",
|
"app-store": "yarn workspace @calcom/app-store-cli cli",
|
||||||
"app-store:build": "yarn workspace @calcom/app-store-cli build",
|
"app-store:build": "yarn workspace @calcom/app-store-cli build",
|
||||||
"turbo-w": "node turbo-wrapper.js"
|
"turbo-w": "node turbo-wrapper.js",
|
||||||
|
"app-e2e-quick": "turbo run app-e2e-quick"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@snaplet/copycat": "^0.3.0",
|
"@snaplet/copycat": "^0.3.0",
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"build": "ts-node --transpile-only src/app-store.ts",
|
"build": "ts-node --transpile-only src/app-store.ts",
|
||||||
"cli": "ts-node --transpile-only src/cli.tsx",
|
"cli": "ts-node --transpile-only src/cli.tsx",
|
||||||
"watch": "ts-node --transpile-only src/app-store.ts --watch",
|
"watch": "ts-node --transpile-only src/app-store.ts --watch",
|
||||||
"generate": "ts-node --transpile-only src/app-store.ts"
|
"generate": "ts-node --transpile-only src/app-store.ts",
|
||||||
|
"post-install": "yarn build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/cli.js"
|
"dist/cli.js"
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
const fs = require("fs");
|
import chokidar from "chokidar";
|
||||||
const path = require("path");
|
import fs from "fs";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
let isInWatchMode = false;
|
let isInWatchMode = false;
|
||||||
if (process.argv[2] === "--watch") {
|
if (process.argv[2] === "--watch") {
|
||||||
isInWatchMode = true;
|
isInWatchMode = true;
|
||||||
}
|
}
|
||||||
const chokidar = require("chokidar");
|
|
||||||
const { debounce } = require("lodash");
|
|
||||||
const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
|
const APP_STORE_PATH = path.join(__dirname, "..", "..", "app-store");
|
||||||
|
type App = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
function getAppName(candidatePath) {
|
function getAppName(candidatePath) {
|
||||||
function isValidAppName(candidatePath) {
|
function isValidAppName(candidatePath) {
|
||||||
if (!candidatePath.startsWith("_") && !candidatePath.includes("/") && !candidatePath.includes("\\")) {
|
if (
|
||||||
|
!candidatePath.startsWith("_") &&
|
||||||
|
candidatePath !== "ee" &&
|
||||||
|
!candidatePath.includes("/") &&
|
||||||
|
!candidatePath.includes("\\")
|
||||||
|
) {
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidAppName(candidatePath)) {
|
if (isValidAppName(candidatePath)) {
|
||||||
// Already a dirname of an app
|
// Already a dirname of an app
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
|
@ -26,36 +36,63 @@ function getAppName(candidatePath) {
|
||||||
function generateFiles() {
|
function generateFiles() {
|
||||||
const browserOutput = [`import dynamic from "next/dynamic"`];
|
const browserOutput = [`import dynamic from "next/dynamic"`];
|
||||||
const serverOutput = [];
|
const serverOutput = [];
|
||||||
const appDirs = [];
|
const appDirs: App[] = [];
|
||||||
|
|
||||||
fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) {
|
fs.readdirSync(`${APP_STORE_PATH}`).forEach(function (dir) {
|
||||||
if (fs.statSync(`${APP_STORE_PATH}/${dir}`).isDirectory()) {
|
if (dir === "ee") {
|
||||||
|
fs.readdirSync(path.join(APP_STORE_PATH, dir)).forEach(function (eeDir) {
|
||||||
|
if (fs.statSync(path.join(APP_STORE_PATH, dir, eeDir)).isDirectory()) {
|
||||||
|
if (!getAppName(path.resolve(eeDir))) {
|
||||||
|
appDirs.push({
|
||||||
|
name: eeDir,
|
||||||
|
path: path.join(dir, eeDir),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (fs.statSync(path.join(APP_STORE_PATH, dir)).isDirectory()) {
|
||||||
if (!getAppName(dir)) {
|
if (!getAppName(dir)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appDirs.push(dir);
|
appDirs.push({
|
||||||
|
name: dir,
|
||||||
|
path: dir,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function forEachAppDir(callback) {
|
function forEachAppDir(callback: (arg: App) => void) {
|
||||||
for (let i = 0; i < appDirs.length; i++) {
|
for (let i = 0; i < appDirs.length; i++) {
|
||||||
callback(appDirs[i]);
|
callback(appDirs[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getObjectExporter(objectName, { fileToBeImported, importBuilder, entryBuilder }) {
|
function getObjectExporter(
|
||||||
|
objectName,
|
||||||
|
{
|
||||||
|
fileToBeImported,
|
||||||
|
importBuilder,
|
||||||
|
entryBuilder,
|
||||||
|
}: {
|
||||||
|
fileToBeImported: string;
|
||||||
|
importBuilder: (arg: App) => string;
|
||||||
|
entryBuilder: (arg: App) => string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
const output = [];
|
const output = [];
|
||||||
forEachAppDir((appName) => {
|
forEachAppDir((app) => {
|
||||||
if (fs.existsSync(path.join(APP_STORE_PATH, appName, fileToBeImported))) {
|
if (fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported))) {
|
||||||
output.push(importBuilder(appName));
|
output.push(importBuilder(app));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
output.push(`export const ${objectName} = {`);
|
output.push(`export const ${objectName} = {`);
|
||||||
|
|
||||||
forEachAppDir((dirName) => {
|
forEachAppDir((app) => {
|
||||||
if (fs.existsSync(path.join(APP_STORE_PATH, dirName, fileToBeImported))) {
|
if (fs.existsSync(path.join(APP_STORE_PATH, app.path, fileToBeImported))) {
|
||||||
output.push(entryBuilder(dirName));
|
output.push(entryBuilder(app));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,25 +103,25 @@ function generateFiles() {
|
||||||
serverOutput.push(
|
serverOutput.push(
|
||||||
...getObjectExporter("apiHandlers", {
|
...getObjectExporter("apiHandlers", {
|
||||||
fileToBeImported: "api/index.ts",
|
fileToBeImported: "api/index.ts",
|
||||||
importBuilder: (appName) => `const ${appName}_api = import("./${appName}/api");`,
|
importBuilder: (app) => `const ${app.name}_api = import("./${app.path}/api");`,
|
||||||
entryBuilder: (appName) => `${appName}:${appName}_api,`,
|
entryBuilder: (app) => `${app.name}:${app.name}_api,`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
browserOutput.push(
|
browserOutput.push(
|
||||||
...getObjectExporter("appStoreMetadata", {
|
...getObjectExporter("appStoreMetadata", {
|
||||||
fileToBeImported: "_metadata.ts",
|
fileToBeImported: "_metadata.ts",
|
||||||
importBuilder: (appName) => `import { metadata as ${appName}_meta } from "./${appName}/_metadata";`,
|
importBuilder: (app) => `import { metadata as ${app.name}_meta } from "./${app.path}/_metadata";`,
|
||||||
entryBuilder: (appName) => `${appName}:${appName}_meta,`,
|
entryBuilder: (app) => `${app.name}:${app.name}_meta,`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
browserOutput.push(
|
browserOutput.push(
|
||||||
...getObjectExporter("InstallAppButtonMap", {
|
...getObjectExporter("InstallAppButtonMap", {
|
||||||
fileToBeImported: "components/InstallAppButton.tsx",
|
fileToBeImported: "components/InstallAppButton.tsx",
|
||||||
importBuilder: (appName) =>
|
importBuilder: (app) =>
|
||||||
`const ${appName}_installAppButton = dynamic(() =>import("./${appName}/components/InstallAppButton"));`,
|
`const ${app.name}_installAppButton = dynamic(() =>import("./${app.path}/components/InstallAppButton"));`,
|
||||||
entryBuilder: (appName) => `${appName}:${appName}_installAppButton,`,
|
entryBuilder: (app) => `${app.name}:${app.name}_installAppButton,`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const banner = `/**
|
const banner = `/**
|
||||||
|
|
|
@ -6,13 +6,18 @@ export async function getAppWithMetadata(app: { dirName: string }) {
|
||||||
try {
|
try {
|
||||||
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App;
|
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
try {
|
||||||
|
appMetadata = (await import(`./ee/${app.dirName}/_metadata`)).default as App;
|
||||||
|
} catch (e) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error(`No metadata found for: "${app.dirName}". Message:`, error.message);
|
console.error(`No metadata found for: "${app.dirName}". Message:`, error.message);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!appMetadata) return null;
|
if (!appMetadata) return null;
|
||||||
// Let's not leak api keys to the front end
|
// Let's not leak api keys to the front end
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { key, ...metadata } = appMetadata;
|
const { key, ...metadata } = appMetadata;
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export async function loginAsUser(username: string, page: Page) {
|
||||||
|
// Skip if file exists
|
||||||
|
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/auth/login`);
|
||||||
|
// Click input[name="email"]
|
||||||
|
await page.click('input[name="email"]');
|
||||||
|
// Fill input[name="email"]
|
||||||
|
await page.fill('input[name="email"]', `${username}@example.com`);
|
||||||
|
// Press Tab
|
||||||
|
await page.press('input[name="email"]', "Tab");
|
||||||
|
// Fill input[name="password"]
|
||||||
|
await page.fill('input[name="password"]', username);
|
||||||
|
// Press Enter
|
||||||
|
await page.press('input[name="password"]', "Enter");
|
||||||
|
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||||
|
// Save signed-in state to '${username}StorageState.json'.
|
||||||
|
await page.context().storageState({ path: `playwright/artifacts/${username}StorageState.json` });
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { PlaywrightTestConfig, devices } from "@playwright/test";
|
||||||
|
import { config as dotEnvConfig } from "dotenv";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
// TODO: May be derive it automatically, so that moving the file to another location doesn't require changing the code
|
||||||
|
dotEnvConfig({ path: "../../../../../.env" });
|
||||||
|
const DEFAULT_NAVIGATION_TIMEOUT = 15000;
|
||||||
|
|
||||||
|
// Paths are relative to main playwright config.
|
||||||
|
const outputDir = path.join("../results");
|
||||||
|
const testDir = path.join("../tests");
|
||||||
|
|
||||||
|
// Quick Mode has no retries to fail fast and quickly re-iterate
|
||||||
|
// Also, it runs the tests only one browser for the same reason
|
||||||
|
const quickMode = process.env.QUICK === "true";
|
||||||
|
const CI = process.env.CI;
|
||||||
|
export const config: PlaywrightTestConfig = {
|
||||||
|
forbidOnly: !!CI,
|
||||||
|
retries: quickMode && !CI ? 0 : 1,
|
||||||
|
workers: 1,
|
||||||
|
timeout: 60_000,
|
||||||
|
reporter: [
|
||||||
|
[CI ? "github" : "list"],
|
||||||
|
[
|
||||||
|
"html",
|
||||||
|
{
|
||||||
|
outputFolder: path.join(process.cwd(), "playwright", "reports", "playwright-html-report"),
|
||||||
|
open: "never",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
["junit", { outputFile: path.join(process.cwd(), "playwright", "reports", "results.xml") }],
|
||||||
|
],
|
||||||
|
outputDir,
|
||||||
|
webServer: {
|
||||||
|
command: "NEXT_PUBLIC_IS_E2E=1 yarn workspace @calcom/web start -p 3000",
|
||||||
|
port: 3000,
|
||||||
|
timeout: 60_000,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
locale: "en-US",
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
testDir,
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
/** If navigation takes more than this, then something's wrong, let's fail fast. */
|
||||||
|
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "../../../../apps/web/playwright/lib/testUtils";
|
|
@ -1,10 +1,8 @@
|
||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
|
|
||||||
import config from "./config.json";
|
import config from "./config.json";
|
||||||
import _package from "./package.json";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
description: _package.description,
|
|
||||||
category: "other",
|
category: "other",
|
||||||
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
|
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
|
||||||
installed: true,
|
installed: true,
|
||||||
|
|
|
@ -4,19 +4,25 @@ import type { IntegrationOAuthCallbackState } from "@calcom/app-store/types";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { App } from "@calcom/types/App";
|
import { App } from "@calcom/types/App";
|
||||||
|
|
||||||
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
|
function useAddAppMutation(_type: App["type"] | null, options?: Parameters<typeof useMutation>[2]) {
|
||||||
const appName = type;
|
const mutation = useMutation<unknown, Error, { type?: App["type"] } | "">(async (variables) => {
|
||||||
const mutation = useMutation(async () => {
|
let type: string | null | undefined;
|
||||||
|
if (variables === "") {
|
||||||
|
type = _type;
|
||||||
|
} else {
|
||||||
|
type = variables.type;
|
||||||
|
}
|
||||||
const state: IntegrationOAuthCallbackState = {
|
const state: IntegrationOAuthCallbackState = {
|
||||||
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
||||||
};
|
};
|
||||||
const stateStr = encodeURIComponent(JSON.stringify(state));
|
const stateStr = encodeURIComponent(JSON.stringify(state));
|
||||||
const searchParams = `?state=${stateStr}`;
|
const searchParams = `?state=${stateStr}`;
|
||||||
|
|
||||||
const res = await fetch(`/api/integrations/${appName}/add` + searchParams);
|
const res = await fetch(`/api/integrations/${type}/add` + searchParams);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Something went wrong");
|
const errorBody = await res.json();
|
||||||
|
throw new Error(errorBody.message || "Something went wrong");
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { metadata as applecalendar_meta } from "./applecalendar/_metadata";
|
||||||
import { metadata as around_meta } from "./around/_metadata";
|
import { metadata as around_meta } from "./around/_metadata";
|
||||||
import { metadata as caldavcalendar_meta } from "./caldavcalendar/_metadata";
|
import { metadata as caldavcalendar_meta } from "./caldavcalendar/_metadata";
|
||||||
import { metadata as dailyvideo_meta } from "./dailyvideo/_metadata";
|
import { metadata as dailyvideo_meta } from "./dailyvideo/_metadata";
|
||||||
|
import { metadata as routing_forms_meta } from "./ee/routing_forms/_metadata";
|
||||||
import { metadata as exchange2013calendar_meta } from "./exchange2013calendar/_metadata";
|
import { metadata as exchange2013calendar_meta } from "./exchange2013calendar/_metadata";
|
||||||
import { metadata as exchange2016calendar_meta } from "./exchange2016calendar/_metadata";
|
import { metadata as exchange2016calendar_meta } from "./exchange2016calendar/_metadata";
|
||||||
import { metadata as giphy_meta } from "./giphy/_metadata";
|
import { metadata as giphy_meta } from "./giphy/_metadata";
|
||||||
|
@ -32,6 +33,7 @@ applecalendar:applecalendar_meta,
|
||||||
around:around_meta,
|
around:around_meta,
|
||||||
caldavcalendar:caldavcalendar_meta,
|
caldavcalendar:caldavcalendar_meta,
|
||||||
dailyvideo:dailyvideo_meta,
|
dailyvideo:dailyvideo_meta,
|
||||||
|
routing_forms:routing_forms_meta,
|
||||||
exchange2013calendar:exchange2013calendar_meta,
|
exchange2013calendar:exchange2013calendar_meta,
|
||||||
exchange2016calendar:exchange2016calendar_meta,
|
exchange2016calendar:exchange2016calendar_meta,
|
||||||
giphy:giphy_meta,
|
giphy:giphy_meta,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
const applecalendar_api = import("./applecalendar/api");
|
const applecalendar_api = import("./applecalendar/api");
|
||||||
const around_api = import("./around/api");
|
const around_api = import("./around/api");
|
||||||
const caldavcalendar_api = import("./caldavcalendar/api");
|
const caldavcalendar_api = import("./caldavcalendar/api");
|
||||||
|
const routing_forms_api = import("./ee/routing_forms/api");
|
||||||
const exchange2013calendar_api = import("./exchange2013calendar/api");
|
const exchange2013calendar_api = import("./exchange2013calendar/api");
|
||||||
const exchange2016calendar_api = import("./exchange2016calendar/api");
|
const exchange2016calendar_api = import("./exchange2016calendar/api");
|
||||||
const giphy_api = import("./giphy/api");
|
const giphy_api = import("./giphy/api");
|
||||||
|
@ -28,6 +29,7 @@ export const apiHandlers = {
|
||||||
applecalendar:applecalendar_api,
|
applecalendar:applecalendar_api,
|
||||||
around:around_api,
|
around:around_api,
|
||||||
caldavcalendar:caldavcalendar_api,
|
caldavcalendar:caldavcalendar_api,
|
||||||
|
routing_forms:routing_forms_api,
|
||||||
exchange2013calendar:exchange2013calendar_api,
|
exchange2013calendar:exchange2013calendar_api,
|
||||||
exchange2016calendar:exchange2016calendar_api,
|
exchange2016calendar:exchange2016calendar_api,
|
||||||
giphy:giphy_api,
|
giphy:giphy_api,
|
||||||
|
|
|
@ -1,39 +1,77 @@
|
||||||
import { useSession } from "next-auth/react";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
||||||
|
|
||||||
import { InstallAppButtonMap } from "./apps.browser.generated";
|
import { InstallAppButtonMap } from "./apps.browser.generated";
|
||||||
import { InstallAppButtonProps } from "./types";
|
import { InstallAppButtonProps } from "./types";
|
||||||
|
|
||||||
export const InstallAppButton = (
|
function InstallAppButtonWithoutPlanCheck(
|
||||||
props: {
|
props: {
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
} & InstallAppButtonProps
|
} & InstallAppButtonProps
|
||||||
) => {
|
) {
|
||||||
const { status } = useSession();
|
|
||||||
const { t } = useLocale();
|
|
||||||
const key = deriveAppDictKeyFromType(props.type, InstallAppButtonMap);
|
const key = deriveAppDictKeyFromType(props.type, InstallAppButtonMap);
|
||||||
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
|
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
|
||||||
if (!InstallAppButtonComponent) return null;
|
if (!InstallAppButtonComponent) return <>{props.render({ useDefaultComponent: true })}</>;
|
||||||
if (status === "unauthenticated")
|
|
||||||
return (
|
|
||||||
<InstallAppButtonComponent
|
|
||||||
render={() => (
|
|
||||||
<Button
|
|
||||||
data-testid="install-app-button"
|
|
||||||
color="primary"
|
|
||||||
href={`${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL + location.pathname + location.search}`}>
|
|
||||||
{t("install_app")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
onChanged={props.onChanged}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
|
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
|
||||||
|
}
|
||||||
|
export const InstallAppButton = (
|
||||||
|
props: {
|
||||||
|
isProOnly?: App["isProOnly"];
|
||||||
|
type: App["type"];
|
||||||
|
} & InstallAppButtonProps
|
||||||
|
) => {
|
||||||
|
const { isLoading, data: user } = trpc.useQuery(["viewer.me"]);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const proProtectionElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const el = proProtectionElementRef.current;
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.addEventListener(
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
|
if (!user) {
|
||||||
|
router.push(
|
||||||
|
`${WEBAPP_URL}/auth/login?callbackUrl=${WEBAPP_URL + location.pathname + location.search}`
|
||||||
|
);
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.plan === "FREE" && props.isProOnly) {
|
||||||
|
setModalOpen(true);
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, [isLoading, user, router, props.isProOnly]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={proProtectionElementRef}>
|
||||||
|
<InstallAppButtonWithoutPlanCheck {...props} />
|
||||||
|
<UpgradeToProDialog modalOpen={modalOpen} setModalOpen={setModalOpen}>
|
||||||
|
{t("app_upgrade_description")}
|
||||||
|
</UpgradeToProDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AppConfiguration } from "./_components/AppConfiguration";
|
export { AppConfiguration } from "./_components/AppConfiguration";
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { App } from "@calcom/types/App";
|
||||||
|
|
||||||
|
import config from "./config.json";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
category: "other",
|
||||||
|
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
|
||||||
|
installed: true,
|
||||||
|
rating: 0,
|
||||||
|
reviews: 0,
|
||||||
|
trending: true,
|
||||||
|
verified: true,
|
||||||
|
licenseRequired: true,
|
||||||
|
isProOnly: true,
|
||||||
|
...config,
|
||||||
|
} as App;
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||||
|
|
||||||
|
import appConfig from "../config.json";
|
||||||
|
|
||||||
|
const handler: AppDeclarativeHandler = {
|
||||||
|
// Instead of passing appType and slug from here, api/integrations/[..args] should be able to derive and pass these directly to createCredential
|
||||||
|
appType: appConfig.type,
|
||||||
|
slug: appConfig.slug,
|
||||||
|
supportsMultipleInstalls: false,
|
||||||
|
handlerType: "add",
|
||||||
|
createCredential: async ({ user, appType, slug }) => {
|
||||||
|
return await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: appType,
|
||||||
|
key: {},
|
||||||
|
userId: user.id,
|
||||||
|
appId: slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
redirectUrl: "/apps/routing_forms/forms",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as add } from "./add";
|
||||||
|
export { default as responses } from "./responses/[formId]";
|
|
@ -0,0 +1,68 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import { Response } from "../../pages/routing-link/[...appPages]";
|
||||||
|
|
||||||
|
function escapeCsvText(str: string) {
|
||||||
|
return str.replace(/,/, "%2C");
|
||||||
|
}
|
||||||
|
async function* getResponses(formId: string) {
|
||||||
|
let responses;
|
||||||
|
let skip = 0;
|
||||||
|
const take = 100;
|
||||||
|
while (
|
||||||
|
(responses = await prisma.app_RoutingForms_FormResponse.findMany({
|
||||||
|
where: {
|
||||||
|
formId,
|
||||||
|
},
|
||||||
|
take: take,
|
||||||
|
skip: skip,
|
||||||
|
})) &&
|
||||||
|
responses.length
|
||||||
|
) {
|
||||||
|
const csv: string[] = [];
|
||||||
|
// Because attributes can be added or removed at any time we can't have fixed columns.
|
||||||
|
// Because there can be huge amount of data we can't keep all that in memory to identify columns from all the data at once.
|
||||||
|
// TODO: So, for now add the field label in front of it. It certainly needs improvement.
|
||||||
|
// TODO: Email CSV when we need to scale it.
|
||||||
|
responses.forEach((response) => {
|
||||||
|
const fieldResponses = response.response as Response;
|
||||||
|
const csvLineColumns = [];
|
||||||
|
for (const [, fieldResponse] of Object.entries(fieldResponses)) {
|
||||||
|
const label = escapeCsvText(fieldResponse.label);
|
||||||
|
const value = fieldResponse.value;
|
||||||
|
let serializedValue = "";
|
||||||
|
if (value instanceof Array) {
|
||||||
|
serializedValue = value.map((val) => escapeCsvText(val)).join(" | ");
|
||||||
|
} else {
|
||||||
|
serializedValue = escapeCsvText(value);
|
||||||
|
}
|
||||||
|
csvLineColumns.push(`"Attribute Label :=> Value"`);
|
||||||
|
csvLineColumns.push(`"${label} :=> ${serializedValue}"`);
|
||||||
|
}
|
||||||
|
csv.push(csvLineColumns.join(","));
|
||||||
|
});
|
||||||
|
skip += take;
|
||||||
|
yield csv.join("\n");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { args } = req.query;
|
||||||
|
if (!args) {
|
||||||
|
throw new Error("args must be set");
|
||||||
|
}
|
||||||
|
const formId = args[2];
|
||||||
|
if (!formId) {
|
||||||
|
throw new Error("formId must be provided");
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", "text/csv; charset=UTF-8");
|
||||||
|
res.setHeader("Transfer-Encoding", "chunked");
|
||||||
|
const csvIterator = getResponses(formId);
|
||||||
|
for await (const partialCsv of csvIterator) {
|
||||||
|
res.write(partialCsv);
|
||||||
|
res.write("\n");
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import NavTabs from "@components/NavTabs";
|
||||||
|
|
||||||
|
import { getSerializableForm } from "../utils";
|
||||||
|
|
||||||
|
export default function RoutingNavBar({
|
||||||
|
form,
|
||||||
|
appUrl,
|
||||||
|
}: {
|
||||||
|
form: ReturnType<typeof getSerializableForm>;
|
||||||
|
appUrl: string;
|
||||||
|
}) {
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: "Form",
|
||||||
|
href: `${appUrl}/form-edit/${form?.id}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routing",
|
||||||
|
href: `${appUrl}/route-builder/${form?.id}`,
|
||||||
|
className: "hidden lg:block",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="mb-4">
|
||||||
|
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
|
|
||||||
|
import Shell from "@components/Shell";
|
||||||
|
|
||||||
|
import RoutingNavBar from "../components/RoutingNavBar";
|
||||||
|
import { getSerializableForm } from "../utils";
|
||||||
|
|
||||||
|
const RoutingShell: React.FC<{
|
||||||
|
form: ReturnType<typeof getSerializableForm>;
|
||||||
|
heading: ReactNode;
|
||||||
|
appUrl: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}> = function RoutingShell({ children, form, heading, appUrl }) {
|
||||||
|
return (
|
||||||
|
<Shell heading={heading} subtitle={form.description || ""}>
|
||||||
|
<div className="-mx-4 px-4 sm:px-6 md:-mx-8 md:px-8">
|
||||||
|
{!form.routes?.length ? (
|
||||||
|
<Alert severity="warning" title="No routes defined yet" message="" className="mb-4" />
|
||||||
|
) : null}
|
||||||
|
{!form.fields.length ? (
|
||||||
|
<Alert severity="warning" title="No attributes defined yet" message="" className="mb-4" />
|
||||||
|
) : null}
|
||||||
|
<div className="bg-gray-50">
|
||||||
|
<RoutingNavBar appUrl={appUrl} form={form} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RoutingShell;
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { ExternalLinkIcon, LinkIcon, DownloadIcon, TrashIcon } from "@heroicons/react/solid";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { Switch } from "@calcom/ui";
|
||||||
|
import { DialogTrigger, Dialog } from "@calcom/ui/Dialog";
|
||||||
|
import { trpc } from "@calcom/web/lib/trpc";
|
||||||
|
|
||||||
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
|
|
||||||
|
import { getSerializableForm } from "../utils";
|
||||||
|
|
||||||
|
export default function SideBar({
|
||||||
|
form,
|
||||||
|
appUrl,
|
||||||
|
}: {
|
||||||
|
form: ReturnType<typeof getSerializableForm>;
|
||||||
|
appUrl: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
const mutation = trpc.useMutation("viewer.app_routing_forms.form", {
|
||||||
|
onSuccess() {
|
||||||
|
router.replace(router.asPath);
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
showToast(`Something went wrong`, "error");
|
||||||
|
},
|
||||||
|
onSettled() {
|
||||||
|
utils.invalidateQueries(["viewer.app_routing_forms.form"]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = trpc.useMutation("viewer.app_routing_forms.deleteForm", {
|
||||||
|
onError() {
|
||||||
|
showToast(`Something went wrong`, "error");
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
router.push(`/${appUrl}/forms`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formLink = `${CAL_URL}/forms/${form.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-0 mt-1 mb-4 w-full lg:w-3/12 lg:px-2 lg:ltr:ml-2 lg:rtl:mr-2">
|
||||||
|
<div className="px-2">
|
||||||
|
<Switch
|
||||||
|
checked={!form.disabled}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
mutation.mutate({ ...form, disabled: !isChecked });
|
||||||
|
}}
|
||||||
|
label={!form.disabled ? t("Disable Form") : t("Enable Form")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 space-y-1.5">
|
||||||
|
<a
|
||||||
|
href={formLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-md inline-flex items-center rounded-sm px-2 py-1 text-sm font-medium text-neutral-700 hover:bg-gray-200 hover:text-gray-900">
|
||||||
|
<ExternalLinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" aria-hidden="true" />
|
||||||
|
{t("preview")}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(formLink);
|
||||||
|
showToast("Link copied!", "success");
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||||
|
<LinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||||
|
{t("Copy link to form")}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={"/api/integrations/routing_forms/responses/" + form.id}
|
||||||
|
download={`${form.name}-${form.id}.csv`}
|
||||||
|
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||||
|
<DownloadIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||||
|
{t("Download responses (CSV)")}
|
||||||
|
</a>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-red-500 hover:bg-gray-200">
|
||||||
|
<TrashIcon className="h-4 w-4 text-red-500 ltr:mr-2 rtl:ml-2" />
|
||||||
|
{t("delete")}
|
||||||
|
</DialogTrigger>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
isLoading={deleteMutation.isLoading}
|
||||||
|
variety="danger"
|
||||||
|
title="Delete Form"
|
||||||
|
confirmBtnText="Yes, delete form"
|
||||||
|
onConfirm={() => {
|
||||||
|
deleteMutation.mutate({ id: form.id });
|
||||||
|
}}>
|
||||||
|
Are you sure you want to delete this form? Anyone who you've shared the link with will no
|
||||||
|
longer be able to book using it.
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as InstallAppButton } from "./InstallAppButton";
|
||||||
|
export { default as Icon } from "./icon";
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder";
|
||||||
|
// Figure out why ee/routing_forms/env.d.ts doesn't work
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
import BasicConfig from "react-awesome-query-builder/lib/config/basic";
|
||||||
|
|
||||||
|
import widgetsComponents from "../widgets";
|
||||||
|
|
||||||
|
const {
|
||||||
|
TextWidget,
|
||||||
|
TextAreaWidget,
|
||||||
|
MultiSelectWidget,
|
||||||
|
SelectWidget,
|
||||||
|
NumberWidget,
|
||||||
|
FieldSelect,
|
||||||
|
Conjs,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Provider,
|
||||||
|
} = widgetsComponents;
|
||||||
|
|
||||||
|
const renderComponent = function <T1>(props: T1 | undefined, Component: React.FC<T1>) {
|
||||||
|
if (!props) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return <Component {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings: Settings = {
|
||||||
|
...BasicConfig.settings,
|
||||||
|
|
||||||
|
renderField: (props) => renderComponent(props, FieldSelect),
|
||||||
|
renderOperator: (props) => renderComponent(props, FieldSelect),
|
||||||
|
renderFunc: (props) => renderComponent(props, FieldSelect),
|
||||||
|
renderConjs: (props) => renderComponent(props, Conjs),
|
||||||
|
renderButton: (props) => renderComponent(props, Button),
|
||||||
|
renderButtonGroup: (props) => renderComponent(props, ButtonGroup),
|
||||||
|
renderProvider: (props) => renderComponent(props, Provider),
|
||||||
|
|
||||||
|
groupActionsPosition: "bottomCenter",
|
||||||
|
|
||||||
|
// Disable groups
|
||||||
|
maxNesting: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// react-query-builder types have missing type property on Widget
|
||||||
|
const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string } } = {
|
||||||
|
...BasicConfig.widgets,
|
||||||
|
text: {
|
||||||
|
...BasicConfig.widgets.text,
|
||||||
|
factory: (props) => renderComponent(props, TextWidget),
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
...BasicConfig.widgets.textarea,
|
||||||
|
factory: (props) => renderComponent(props, TextAreaWidget),
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
...BasicConfig.widgets.number,
|
||||||
|
factory: (props) => renderComponent(props, NumberWidget),
|
||||||
|
},
|
||||||
|
multiselect: {
|
||||||
|
...BasicConfig.widgets.multiselect,
|
||||||
|
factory: (
|
||||||
|
props: SelectWidgetProps & {
|
||||||
|
listValues: { title: string; value: string }[];
|
||||||
|
}
|
||||||
|
) => renderComponent(props, MultiSelectWidget),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...BasicConfig.widgets.select,
|
||||||
|
factory: (
|
||||||
|
props: SelectWidgetProps & {
|
||||||
|
listValues: { title: string; value: string }[];
|
||||||
|
}
|
||||||
|
) => renderComponent(props, SelectWidget),
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
...BasicConfig.widgets.text,
|
||||||
|
factory: (props) => {
|
||||||
|
if (!props) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return <TextWidget type="tel" {...props} />;
|
||||||
|
},
|
||||||
|
valuePlaceholder: "Enter Phone Number",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
...BasicConfig.widgets.text,
|
||||||
|
factory: (props) => {
|
||||||
|
if (!props) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return <TextWidget type="email" {...props} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
...BasicConfig.types,
|
||||||
|
phone: {
|
||||||
|
...BasicConfig.types.text,
|
||||||
|
widgets: {
|
||||||
|
...BasicConfig.types.text.widgets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
...BasicConfig.types.text,
|
||||||
|
widgets: {
|
||||||
|
...BasicConfig.types.text.widgets,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const operators = BasicConfig.operators;
|
||||||
|
operators.equal.label = operators.select_equals.label = "Equals";
|
||||||
|
operators.greater_or_equal.label = "Greater than or equal to";
|
||||||
|
operators.greater.label = "Greater than";
|
||||||
|
operators.less_or_equal.label = "Less than or equal to";
|
||||||
|
operators.less.label = "Less than";
|
||||||
|
operators.not_equal.label = operators.select_not_equals.label = "Does not equal";
|
||||||
|
operators.between.label = "Between";
|
||||||
|
|
||||||
|
delete operators.proximity;
|
||||||
|
delete operators.is_null;
|
||||||
|
delete operators.is_not_null;
|
||||||
|
const config = {
|
||||||
|
conjunctions: BasicConfig.conjunctions,
|
||||||
|
operators,
|
||||||
|
types,
|
||||||
|
widgets,
|
||||||
|
settings,
|
||||||
|
};
|
||||||
|
export default config;
|
|
@ -0,0 +1,125 @@
|
||||||
|
.cal-query-builder .query-builder,
|
||||||
|
.cal-query-builder .qb-draggable,
|
||||||
|
.cal-query-builder .qb-drag-handler {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hide connectors */
|
||||||
|
.cal-query-builder .group-or-rule::before,
|
||||||
|
.cal-query-builder .group-or-rule::after {
|
||||||
|
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||||
|
content: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .group--children {
|
||||||
|
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide "and" for between numbers */
|
||||||
|
.cal-query-builder .widget--sep {
|
||||||
|
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout of all fields- Distance b/w them, positioning, width */
|
||||||
|
.cal-query-builder .rule--body--wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--field,
|
||||||
|
.cal-query-builder .rule--operator,
|
||||||
|
.cal-query-builder .rule--value {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--widget {
|
||||||
|
display: "inline-block";
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .widget--widget,
|
||||||
|
.cal-query-builder .widget--widget,
|
||||||
|
.cal-query-builder .widget--widget > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--drag-handler,
|
||||||
|
.cal-query-builder .rule--header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 8px;
|
||||||
|
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--func--wrapper,
|
||||||
|
.cal-query-builder .rule--func,
|
||||||
|
.cal-query-builder .rule--func--args,
|
||||||
|
.cal-query-builder .rule--func--arg,
|
||||||
|
.cal-query-builder .rule--func--arg-value,
|
||||||
|
.cal-query-builder .rule--func--bracket-before,
|
||||||
|
.cal-query-builder .rule--func--bracket-after,
|
||||||
|
.cal-query-builder .rule--func--arg-sep,
|
||||||
|
.cal-query-builder .rule--func--arg-label,
|
||||||
|
.cal-query-builder .rule--func--arg-label-sep {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--field,
|
||||||
|
.cal-query-builder .group--field,
|
||||||
|
.cal-query-builder .rule--operator,
|
||||||
|
.cal-query-builder .rule--value,
|
||||||
|
.cal-query-builder .rule--operator-options,
|
||||||
|
.cal-query-builder .widget--widget,
|
||||||
|
.cal-query-builder .widget--valuesrc,
|
||||||
|
.cal-query-builder .operator--options--sep,
|
||||||
|
.cal-query-builder .rule--before-widget,
|
||||||
|
.cal-query-builder .rule--after-widget {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule--operator,
|
||||||
|
.cal-query-builder .widget--widget,
|
||||||
|
.cal-query-builder .widget--valuesrc,
|
||||||
|
.cal-query-builder .widget--sep {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .widget--valuesrc {
|
||||||
|
margin-right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .group--header,
|
||||||
|
.cal-query-builder .group--footer {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .group-or-rule-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cal-query-builder .rule {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 10px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
|
@ -0,0 +1,300 @@
|
||||||
|
import { TrashIcon } from "@heroicons/react/solid";
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
import {
|
||||||
|
FieldProps,
|
||||||
|
ConjsProps,
|
||||||
|
ButtonProps,
|
||||||
|
ButtonGroupProps,
|
||||||
|
ProviderProps,
|
||||||
|
SelectWidgetProps,
|
||||||
|
NumberWidgetProps,
|
||||||
|
TextWidgetProps,
|
||||||
|
} from "react-awesome-query-builder";
|
||||||
|
|
||||||
|
import { Button as CalButton } from "@calcom/ui";
|
||||||
|
import { Input } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
// import { mapListValues } from "../../../../utils/stuff";
|
||||||
|
import { SelectWithValidation as Select } from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
const TextAreaWidget = (props: TextWidgetProps) => {
|
||||||
|
const { value, setValue, readonly, placeholder, maxLength, customProps, ...remainingProps } = props;
|
||||||
|
|
||||||
|
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setValue(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const textValue = value || "";
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
value={textValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={readonly}
|
||||||
|
onChange={onChange}
|
||||||
|
maxLength={maxLength}
|
||||||
|
className="flex flex-grow border-gray-300 text-sm"
|
||||||
|
{...customProps}
|
||||||
|
{...remainingProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextWidget = (props: TextWidgetProps & { type?: string }) => {
|
||||||
|
const { value, setValue, readonly, placeholder, customProps, ...remainingProps } = props;
|
||||||
|
let { type } = props;
|
||||||
|
type = type || "text";
|
||||||
|
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setValue(val);
|
||||||
|
};
|
||||||
|
const textValue = value || "";
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className="flex flex-grow border-gray-300 text-sm"
|
||||||
|
value={textValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={readonly}
|
||||||
|
onChange={onChange}
|
||||||
|
{...remainingProps}
|
||||||
|
{...customProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function NumberWidget({ value, setValue, ...remainingProps }: NumberWidgetProps) {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
name="query-builder"
|
||||||
|
type="number"
|
||||||
|
className="mt-0"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
}}
|
||||||
|
{...remainingProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelectWidget = ({
|
||||||
|
listValues,
|
||||||
|
setValue,
|
||||||
|
value,
|
||||||
|
...remainingProps
|
||||||
|
}: SelectWidgetProps & {
|
||||||
|
listValues: { title: string; value: string }[];
|
||||||
|
}) => {
|
||||||
|
//TODO: Use Select here.
|
||||||
|
//TODO: Let's set listValue itself as label and value instead of using title.
|
||||||
|
if (!listValues) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const selectItems = listValues.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultValue = selectItems.filter((item) => value?.value?.includes(item.value));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
||||||
|
menuPosition="fixed"
|
||||||
|
onChange={(items) => {
|
||||||
|
setValue(items?.map((item) => item.value));
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
isMulti={true}
|
||||||
|
options={selectItems}
|
||||||
|
{...remainingProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SelectWidget({
|
||||||
|
listValues,
|
||||||
|
setValue,
|
||||||
|
value,
|
||||||
|
...remainingProps
|
||||||
|
}: SelectWidgetProps & {
|
||||||
|
listValues: { title: string; value: string }[];
|
||||||
|
}) {
|
||||||
|
if (!listValues) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const selectItems = listValues.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const defaultValue = selectItems.find((item) => item.value === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="data-testid-select block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
||||||
|
menuPosition="fixed"
|
||||||
|
onChange={(item) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(item.value);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
options={selectItems}
|
||||||
|
{...remainingProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Button({ type, label, onClick, readonly }: ButtonProps) {
|
||||||
|
if (type === "delRule" || type == "delGroup") {
|
||||||
|
return (
|
||||||
|
<button className="ml-5">
|
||||||
|
<TrashIcon className="m-0 h-4 w-4 text-neutral-500" onClick={onClick} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let dataTestId = "";
|
||||||
|
if (type === "addRule") {
|
||||||
|
label = "Add rule";
|
||||||
|
dataTestId = "add-rule";
|
||||||
|
} else if (type == "addGroup") {
|
||||||
|
label = "Add rule group";
|
||||||
|
dataTestId = "add-rule-group";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CalButton
|
||||||
|
data-testid={dataTestId}
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={readonly}
|
||||||
|
onClick={onClick}>
|
||||||
|
{label}
|
||||||
|
</CalButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ButtonGroup({ children }: ButtonGroupProps) {
|
||||||
|
if (!(children instanceof Array)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{children.map((button) => {
|
||||||
|
if (!button) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Conjs({ not, setNot, config, conjunctionOptions, setConjunction, disabled }: ConjsProps) {
|
||||||
|
if (!config || !conjunctionOptions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const conjsCount = Object.keys(conjunctionOptions).length;
|
||||||
|
|
||||||
|
const lessThenTwo = disabled;
|
||||||
|
const { forceShowConj } = config.settings;
|
||||||
|
const showConj = forceShowConj || (conjsCount > 1 && !lessThenTwo);
|
||||||
|
const options = [
|
||||||
|
{ label: "All", value: "all" },
|
||||||
|
{ label: "Any", value: "any" },
|
||||||
|
{ label: "None", value: "none" },
|
||||||
|
];
|
||||||
|
const renderOptions = () => {
|
||||||
|
const { checked: andSelected } = conjunctionOptions["AND"];
|
||||||
|
const { checked: orSelected } = conjunctionOptions["OR"];
|
||||||
|
const notSelected = not;
|
||||||
|
// Default to All
|
||||||
|
let value = andSelected ? "all" : orSelected ? "any" : "all";
|
||||||
|
|
||||||
|
if (notSelected) {
|
||||||
|
// not of All -> None
|
||||||
|
// not of Any -> All
|
||||||
|
value = value == "any" ? "none" : "all";
|
||||||
|
}
|
||||||
|
const selectValue = options.find((option) => option.value === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center text-sm">
|
||||||
|
<span>Rule group when</span>
|
||||||
|
<Select
|
||||||
|
className="flex px-2"
|
||||||
|
defaultValue={selectValue}
|
||||||
|
options={options}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) return;
|
||||||
|
if (option.value === "all") {
|
||||||
|
setConjunction("AND");
|
||||||
|
setNot(false);
|
||||||
|
} else if (option.value === "any") {
|
||||||
|
setConjunction("OR");
|
||||||
|
setNot(false);
|
||||||
|
} else if (option.value === "none") {
|
||||||
|
setConjunction("OR");
|
||||||
|
setNot(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>match</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return showConj ? renderOptions() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldSelect = function FieldSelect(props: FieldProps) {
|
||||||
|
const { items, setField, selectedKey } = props;
|
||||||
|
const selectItems = items.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
value: item.key,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultValue = selectItems.find((item) => {
|
||||||
|
return item.value === selectedKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="data-testid-field-select"
|
||||||
|
menuPosition="fixed"
|
||||||
|
onChange={(item) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setField(item.value);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
options={selectItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Provider = ({ children }: ProviderProps) => children;
|
||||||
|
|
||||||
|
const widgets = {
|
||||||
|
TextWidget,
|
||||||
|
TextAreaWidget,
|
||||||
|
SelectWidget,
|
||||||
|
NumberWidget,
|
||||||
|
MultiSelectWidget,
|
||||||
|
FieldSelect,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Conjs,
|
||||||
|
Provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widgets;
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||||
|
"name": "Routing Forms",
|
||||||
|
"title": "Routing Forms",
|
||||||
|
"slug": "routing_forms",
|
||||||
|
"type": "routing_forms_other",
|
||||||
|
"imageSrc": "/api/app-store/ee/routing_forms/icon.svg",
|
||||||
|
"logo": "/api/app-store/ee/routing_forms/icon.svg",
|
||||||
|
"url": "https://cal.com/apps/routing_forms",
|
||||||
|
"variant": "other",
|
||||||
|
"categories": ["other"],
|
||||||
|
"publisher": "Cal.com",
|
||||||
|
"email": "help@cal.com",
|
||||||
|
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user",
|
||||||
|
"__createdUsingCli": true
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
declare module "react-awesome-query-builder/lib/config/basic";
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * as api from "./api";
|
||||||
|
export * as components from "./components";
|
||||||
|
export { metadata } from "./_metadata";
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"name": "@calcom/routing_forms",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user ",
|
||||||
|
"scripts": {
|
||||||
|
"app-e2e": "yarn playwright test --config=playwright/config/playwright.config.ts",
|
||||||
|
"app-e2e-quick": "QUICK=true yarn app-e2e"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@calcom/lib": "*",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"json-logic-js": "^2.0.2",
|
||||||
|
"playwright": "^1.22.2",
|
||||||
|
"react-awesome-query-builder": "^5.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/types": "*",
|
||||||
|
"@types/json-logic-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
//TODO: Generate this file automatically so that like in Next.js file based routing can work automatically
|
||||||
|
import * as formEdit from "./form-edit/[...appPages]";
|
||||||
|
import * as forms from "./forms/[...appPages]";
|
||||||
|
import * as RouteBuilder from "./route-builder/[...appPages]";
|
||||||
|
import * as RoutingLink from "./routing-link/[...appPages]";
|
||||||
|
|
||||||
|
const routingConfig = {
|
||||||
|
"form-edit": formEdit,
|
||||||
|
"route-builder": RouteBuilder,
|
||||||
|
forms: forms,
|
||||||
|
"routing-link": RoutingLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default routingConfig;
|
|
@ -0,0 +1,402 @@
|
||||||
|
import { TrashIcon, PlusIcon, ArrowUpIcon, CollectionIcon, ArrowDownIcon } from "@heroicons/react/solid";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useForm, UseFormReturn, useFieldArray, Controller } from "react-hook-form";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
|
||||||
|
import { Button, Select, BooleanToggleGroup, EmptyScreen } from "@calcom/ui";
|
||||||
|
import { Form, TextArea } from "@calcom/ui/form/fields";
|
||||||
|
import { trpc } from "@calcom/web/lib/trpc";
|
||||||
|
|
||||||
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PencilEdit from "@components/PencilEdit";
|
||||||
|
|
||||||
|
import RoutingShell from "../../components/RoutingShell";
|
||||||
|
import SideBar from "../../components/SideBar";
|
||||||
|
import { getSerializableForm } from "../../utils";
|
||||||
|
|
||||||
|
export const FieldTypes = [
|
||||||
|
{
|
||||||
|
label: "Short Text",
|
||||||
|
value: "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Number",
|
||||||
|
value: "number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Long Text",
|
||||||
|
value: "textarea",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Select",
|
||||||
|
value: "select",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "MultiSelect",
|
||||||
|
value: "multiselect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Phone",
|
||||||
|
value: "phone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Email",
|
||||||
|
value: "email",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
hookForm,
|
||||||
|
hookFieldNamespace,
|
||||||
|
deleteField,
|
||||||
|
moveUp,
|
||||||
|
moveDown,
|
||||||
|
}: {
|
||||||
|
hookForm: UseFormReturn<inferSSRProps<typeof getServerSideProps>["form"]>;
|
||||||
|
hookFieldNamespace: `fields.${number}`;
|
||||||
|
deleteField: {
|
||||||
|
check: () => boolean;
|
||||||
|
fn: () => void;
|
||||||
|
};
|
||||||
|
moveUp: {
|
||||||
|
check: () => boolean;
|
||||||
|
fn: () => void;
|
||||||
|
};
|
||||||
|
moveDown: {
|
||||||
|
check: () => boolean;
|
||||||
|
fn: () => void;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="attribute"
|
||||||
|
className="group mb-4 flex w-full items-center justify-between hover:bg-neutral-50 ltr:mr-2 rtl:ml-2">
|
||||||
|
{moveUp.check() ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="invisible absolute left-1/2 -mt-4 mb-4 -ml-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:left-[19px] sm:ml-0 sm:block"
|
||||||
|
onClick={() => moveUp.fn()}>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{moveDown.check() ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="invisible absolute left-1/2 mt-8 -ml-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:left-[19px] sm:ml-0 sm:block"
|
||||||
|
onClick={() => moveDown.fn()}>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<div className="-mx-4 flex flex-1 items-center rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mt-2 block items-center sm:flex">
|
||||||
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
|
<label htmlFor="label" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||||
|
Label
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
{...hookForm.register(`${hookFieldNamespace}.label`)}
|
||||||
|
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 block items-center sm:flex">
|
||||||
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
|
<label htmlFor="label" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
name={`${hookFieldNamespace}.type`}
|
||||||
|
control={hookForm.control}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className="data-testid-attribute-type"
|
||||||
|
options={FieldTypes}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(option.value);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{["select", "multiselect"].includes(hookForm.watch(`${hookFieldNamespace}.type`)) ? (
|
||||||
|
<div className="mt-2 block items-center sm:flex">
|
||||||
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
|
<label htmlFor="label" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||||
|
Options
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<TextArea
|
||||||
|
placeholder="Add 1 option per line"
|
||||||
|
{...hookForm.register(`${hookFieldNamespace}.selectText`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-2 block items-center sm:flex">
|
||||||
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
|
<label htmlFor="label" className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||||
|
Required
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
name={`${hookFieldNamespace}.required`}
|
||||||
|
control={hookForm.control}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
return <BooleanToggleGroup value={value} onValueChange={onChange} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{deleteField.check() ? (
|
||||||
|
<button
|
||||||
|
className="float-right ml-5"
|
||||||
|
onClick={() => {
|
||||||
|
deleteField.fn();
|
||||||
|
}}
|
||||||
|
color="secondary">
|
||||||
|
<TrashIcon className="h-4 w-4 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FormEdit({
|
||||||
|
form,
|
||||||
|
appUrl,
|
||||||
|
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
const mutation = trpc.useMutation("viewer.app_routing_forms.form", {
|
||||||
|
onError() {
|
||||||
|
showToast(`Something went wrong`, "error");
|
||||||
|
},
|
||||||
|
onSettled() {
|
||||||
|
utils.invalidateQueries([
|
||||||
|
"viewer.app_routing_forms.form",
|
||||||
|
{
|
||||||
|
id: form.id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
showToast(`Form updated successfully.`, "success");
|
||||||
|
router.replace(router.asPath);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldsNamespace = "fields";
|
||||||
|
const hookForm = useForm({
|
||||||
|
defaultValues: form,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
fields: hookFormFields,
|
||||||
|
append: appendHookFormField,
|
||||||
|
remove: removeHookFormField,
|
||||||
|
swap: swapHookFormField,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore https://github.com/react-hook-form/react-hook-form/issues/6679
|
||||||
|
} = useFieldArray({
|
||||||
|
control: hookForm.control,
|
||||||
|
name: fieldsNamespace,
|
||||||
|
});
|
||||||
|
|
||||||
|
// hookForm.reset(form);
|
||||||
|
if (!form.fields) {
|
||||||
|
form.fields = [];
|
||||||
|
}
|
||||||
|
const addAttribute = () => {
|
||||||
|
appendHookFormField({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
id: uuidv4(),
|
||||||
|
// This is same type from react-awesome-query-builder
|
||||||
|
type: "text",
|
||||||
|
label: "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoutingShell
|
||||||
|
form={form}
|
||||||
|
appUrl={appUrl}
|
||||||
|
heading={
|
||||||
|
<PencilEdit
|
||||||
|
value={
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
hookForm.watch("name")
|
||||||
|
}
|
||||||
|
onChange={(value) => {
|
||||||
|
hookForm.setValue("name", value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
{hookFormFields.length ? (
|
||||||
|
<div className="flex flex-col-reverse lg:flex-row">
|
||||||
|
<Form
|
||||||
|
className="w-full max-w-4xl ltr:mr-2 rtl:ml-2 md:w-9/12"
|
||||||
|
form={hookForm}
|
||||||
|
handleSubmit={(data) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
mutation.mutate({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<div className="mb-5">
|
||||||
|
<h3 className="mb-2 text-base font-medium leading-6 text-gray-900">Description</h3>
|
||||||
|
<div className="w-full">
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
data-testid="description"
|
||||||
|
className="block w-full rounded-sm border-gray-300 text-sm "
|
||||||
|
placeholder="Form Description"
|
||||||
|
{...hookForm.register("description")}
|
||||||
|
defaultValue={form.description || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="mb-5 border-neutral-200" />
|
||||||
|
<h3 className="mb-2 text-base font-medium leading-6 text-gray-900">Attributes</h3>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{hookFormFields.map((field, key) => {
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
hookForm={hookForm}
|
||||||
|
hookFieldNamespace={`${fieldsNamespace}.${key}`}
|
||||||
|
deleteField={{
|
||||||
|
check: () => hookFormFields.length > 1,
|
||||||
|
fn: () => {
|
||||||
|
removeHookFormField(key);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
moveUp={{
|
||||||
|
check: () => key !== 0,
|
||||||
|
fn: () => {
|
||||||
|
swapHookFormField(key, key - 1);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
moveDown={{
|
||||||
|
check: () => key !== hookFormFields.length - 1,
|
||||||
|
fn: () => {
|
||||||
|
if (key === hookFormFields.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
swapHookFormField(key, key + 1);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
key={key}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{hookFormFields.length ? (
|
||||||
|
<div className={classNames("flex")}>
|
||||||
|
<Button
|
||||||
|
data-testid="add-attribute"
|
||||||
|
type="button"
|
||||||
|
StartIcon={PlusIcon}
|
||||||
|
color="secondary"
|
||||||
|
onClick={addAttribute}>
|
||||||
|
Add Attribute
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{hookFormFields.length ? (
|
||||||
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
|
<Button href="/apps/routing_forms/forms" color="secondary" tabIndex={-1}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" data-testid="update-form" disabled={mutation.isLoading}>
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Form>
|
||||||
|
<SideBar form={form} appUrl={appUrl} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button data-testid="add-attribute" onClick={addAttribute} className="w-full">
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={CollectionIcon}
|
||||||
|
headline="Create your first attribute"
|
||||||
|
description="Attributes are the form fields that the booker would see."
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</RoutingShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async function getServerSideProps(
|
||||||
|
context: AppGetServerSidePropsContext,
|
||||||
|
prisma: AppPrisma,
|
||||||
|
user: AppUser
|
||||||
|
) {
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/auth/login",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { params } = context;
|
||||||
|
if (!params) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const formId = params.appPages[0];
|
||||||
|
if (!formId || params.appPages.length > 1) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||||
|
where: {
|
||||||
|
id: formId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
form: getSerializableForm(form),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,267 @@
|
||||||
|
import {
|
||||||
|
TrashIcon,
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
DuplicateIcon,
|
||||||
|
PencilIcon,
|
||||||
|
PlusIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
CollectionIcon,
|
||||||
|
} from "@heroicons/react/solid";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
|
||||||
|
import { Button, EmptyScreen, Tooltip } from "@calcom/ui";
|
||||||
|
import Dropdown, {
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@calcom/ui/Dropdown";
|
||||||
|
import { trpc } from "@calcom/web/lib/trpc";
|
||||||
|
|
||||||
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import Shell from "@components/Shell";
|
||||||
|
|
||||||
|
import { getSerializableForm } from "../../utils";
|
||||||
|
|
||||||
|
export default function RoutingForms({
|
||||||
|
forms,
|
||||||
|
appUrl,
|
||||||
|
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const deleteMutation = trpc.useMutation("viewer.app_routing_forms.deleteForm", {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast("Form deleted", "success");
|
||||||
|
router.replace(router.asPath);
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.invalidateQueries(["viewer.app_routing_forms.forms"]);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast("Something went wrong", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const mutation = trpc.useMutation("viewer.app_routing_forms.form", {
|
||||||
|
onSuccess: (_data, variables) => {
|
||||||
|
utils.invalidateQueries("viewer.app_routing_forms.forms");
|
||||||
|
router.push(`${appUrl}/form-edit/${variables.id}`);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast(`Something went wrong`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const formId = uuidv4();
|
||||||
|
return (
|
||||||
|
<Shell
|
||||||
|
heading="Routing Forms"
|
||||||
|
CTA={
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const form = {
|
||||||
|
id: formId,
|
||||||
|
name: `Form-${formId.slice(0, 8)}`,
|
||||||
|
};
|
||||||
|
mutation.mutate(form);
|
||||||
|
}}
|
||||||
|
data-testid="new-routing-form"
|
||||||
|
StartIcon={PlusIcon}>
|
||||||
|
New Form
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
subtitle="You can see all routing forms and create one here.">
|
||||||
|
<div className="-mx-4 md:-mx-8">
|
||||||
|
<div className="mb-10 w-full bg-gray-50 px-4 pb-2 sm:px-6 md:px-8">
|
||||||
|
{!forms.length ? (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={CollectionIcon}
|
||||||
|
headline="Create your first form"
|
||||||
|
description="Forms enable you to allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{forms.length ? (
|
||||||
|
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
|
||||||
|
<ul data-testid="routing-forms-list" className="divide-y divide-neutral-200">
|
||||||
|
{forms.map((form, index) => {
|
||||||
|
if (!form) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const formLink = `${CAL_URL}/forms/${form.id}`;
|
||||||
|
const description = form.description || "";
|
||||||
|
const disabled = form.disabled;
|
||||||
|
form.routes = form.routes || [];
|
||||||
|
const fields = form.fields || [];
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"flex items-center justify-between hover:bg-neutral-50",
|
||||||
|
disabled ? "hover:bg-white" : ""
|
||||||
|
)}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"group flex w-full items-center justify-between px-4 py-4 hover:bg-neutral-50 sm:px-6",
|
||||||
|
disabled ? "hover:bg-white" : ""
|
||||||
|
)}>
|
||||||
|
<Link href={appUrl + "/form-edit/" + form.id}>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
"flex-grow truncate text-sm",
|
||||||
|
disabled ? "pointer-events-none cursor-not-allowed opacity-30" : ""
|
||||||
|
)}>
|
||||||
|
<div className="font-medium text-neutral-900 ltr:mr-1 rtl:ml-1">
|
||||||
|
{form.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-white">
|
||||||
|
<h2 className="max-w-[280px] overflow-hidden text-ellipsis pb-2 opacity-60 sm:max-w-[500px]">
|
||||||
|
{description.substring(0, 100)}
|
||||||
|
{description.length > 100 && "..."}
|
||||||
|
</h2>
|
||||||
|
<div className="mt-2 text-neutral-500 dark:text-white">
|
||||||
|
{fields.length} attributes, {form.routes.length} routes &{" "}
|
||||||
|
{form._count.responses} Responses
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"flex justify-between space-x-2 rtl:space-x-reverse ",
|
||||||
|
disabled && "pointer-events-none cursor-not-allowed"
|
||||||
|
)}>
|
||||||
|
<Tooltip content={t("preview") as string}>
|
||||||
|
<a
|
||||||
|
href={formLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={classNames(
|
||||||
|
"btn-icon appearance-none",
|
||||||
|
disabled && " opacity-30"
|
||||||
|
)}>
|
||||||
|
<ExternalLinkIcon
|
||||||
|
className={classNames("h-5 w-5", !disabled && "group-hover:text-black")}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={t("copy_link") as string}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
showToast(t("link_copied"), "success");
|
||||||
|
navigator.clipboard.writeText(formLink);
|
||||||
|
}}
|
||||||
|
className={classNames("btn-icon", disabled && " opacity-30")}>
|
||||||
|
<LinkIcon
|
||||||
|
className={classNames("h-5 w-5", !disabled && "group-hover:text-black")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger className="h-10 w-10 cursor-pointer rounded-sm border border-transparent text-neutral-500 hover:border-gray-300 hover:text-neutral-900 focus:border-gray-300">
|
||||||
|
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Link href={appUrl + "/form-edit/" + form.id} passHref={true}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
color="minimal"
|
||||||
|
className={classNames("w-full rounded-none")}
|
||||||
|
StartIcon={PencilIcon}>
|
||||||
|
{t("edit")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="minimal"
|
||||||
|
size="sm"
|
||||||
|
className={classNames("hidden w-full rounded-none")}
|
||||||
|
StartIcon={DuplicateIcon}>
|
||||||
|
{t("duplicate")}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteMutation.mutate({
|
||||||
|
id: form.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
color="warn"
|
||||||
|
size="sm"
|
||||||
|
StartIcon={TrashIcon}
|
||||||
|
className="w-full rounded-none">
|
||||||
|
{t("delete")}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async function getServerSideProps(
|
||||||
|
context: AppGetServerSidePropsContext,
|
||||||
|
prisma: AppPrisma,
|
||||||
|
user: AppUser
|
||||||
|
) {
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/auth/login",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const forms = await prisma.app_RoutingForms_Form.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
responses: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serializableForms = forms.map((form) => getSerializableForm(form));
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
forms: serializableForms,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,518 @@
|
||||||
|
import { PlusIcon, TrashIcon, ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/solid";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { useState, useCallback } from "react";
|
||||||
|
import { Query, Config, Builder, Utils as QbUtils } from "react-awesome-query-builder";
|
||||||
|
// types
|
||||||
|
import { JsonTree, ImmutableTree, BuilderProps } from "react-awesome-query-builder";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { Button } from "@calcom/ui";
|
||||||
|
import { Label } from "@calcom/ui/form/fields";
|
||||||
|
import { trpc } from "@calcom/web/lib/trpc";
|
||||||
|
|
||||||
|
import PencilEdit from "@components/PencilEdit";
|
||||||
|
import { SelectWithValidation as Select } from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
import RoutingShell from "../../components/RoutingShell";
|
||||||
|
import SideBar from "../../components/SideBar";
|
||||||
|
import QueryBuilderInitialConfig from "../../components/react-awesome-query-builder/config/config";
|
||||||
|
import "../../components/react-awesome-query-builder/styles.css";
|
||||||
|
import { getSerializableForm } from "../../utils";
|
||||||
|
import { FieldTypes } from "../form-edit/[...appPages]";
|
||||||
|
|
||||||
|
const InitialConfig = QueryBuilderInitialConfig;
|
||||||
|
type QueryBuilderUpdatedConfig = typeof QueryBuilderInitialConfig & { fields: Config["fields"] };
|
||||||
|
export function getQueryBuilderConfig(form: inferSSRProps<typeof getServerSideProps>["form"]) {
|
||||||
|
const fields: Record<string, any> = {};
|
||||||
|
form.fields?.forEach((field) => {
|
||||||
|
if (FieldTypes.map((f) => f.value).includes(field.type)) {
|
||||||
|
const optionValues = field.selectText?.trim().split("\n");
|
||||||
|
const options = optionValues?.map((value) => {
|
||||||
|
const title = value;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const widget = InitialConfig.widgets[field.type];
|
||||||
|
const widgetType = widget.type;
|
||||||
|
|
||||||
|
fields[field.id] = {
|
||||||
|
label: field.label,
|
||||||
|
type: widgetType,
|
||||||
|
valueSources: ["value"],
|
||||||
|
fieldSettings: {
|
||||||
|
listValues: options,
|
||||||
|
},
|
||||||
|
// preferWidgets: field.type === "textarea" ? ["textarea"] : [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported field type:" + field.type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// You need to provide your own config. See below 'Config format'
|
||||||
|
const config: QueryBuilderUpdatedConfig = {
|
||||||
|
...InitialConfig,
|
||||||
|
fields: fields,
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEmptyRoute = (): SerializableRoute => {
|
||||||
|
const uuid = QbUtils.uuid();
|
||||||
|
return {
|
||||||
|
id: uuid,
|
||||||
|
action: {
|
||||||
|
type: "eventTypeRedirectUrl",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
queryValue: { id: uuid, type: "group" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFallbackRoute = (): SerializableRoute => {
|
||||||
|
const uuid = QbUtils.uuid();
|
||||||
|
return {
|
||||||
|
id: uuid,
|
||||||
|
isFallback: true,
|
||||||
|
action: {
|
||||||
|
type: "customPageMessage",
|
||||||
|
value: "Thank you for your interest! We will be in touch soon.",
|
||||||
|
},
|
||||||
|
queryValue: { id: uuid, type: "group" },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type Route = {
|
||||||
|
id: string;
|
||||||
|
isFallback?: boolean;
|
||||||
|
action: {
|
||||||
|
type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl";
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
// This is what's persisted
|
||||||
|
queryValue: JsonTree;
|
||||||
|
// `queryValue` is parsed to create state
|
||||||
|
state: {
|
||||||
|
tree: ImmutableTree;
|
||||||
|
config: QueryBuilderUpdatedConfig;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type SerializableRoute = Pick<Route, "id" | "action"> & {
|
||||||
|
queryValue: Route["queryValue"];
|
||||||
|
isFallback?: Route["isFallback"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const Route = ({
|
||||||
|
route,
|
||||||
|
routes,
|
||||||
|
setRoute,
|
||||||
|
config,
|
||||||
|
setRoutes,
|
||||||
|
moveUp,
|
||||||
|
moveDown,
|
||||||
|
}: {
|
||||||
|
route: Route;
|
||||||
|
routes: Route[];
|
||||||
|
setRoute: (id: string, route: Partial<Route>) => void;
|
||||||
|
config: QueryBuilderUpdatedConfig;
|
||||||
|
setRoutes: React.Dispatch<React.SetStateAction<Route[]>>;
|
||||||
|
moveUp?: { fn: () => void; check: () => boolean } | null;
|
||||||
|
moveDown?: { fn: () => void; check: () => boolean } | null;
|
||||||
|
}) => {
|
||||||
|
const index = routes.indexOf(route);
|
||||||
|
const RoutingPages: { label: string; value: Route["action"]["type"] }[] = [
|
||||||
|
{
|
||||||
|
label: "Custom Page",
|
||||||
|
value: "customPageMessage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "External Redirect",
|
||||||
|
value: "externalRedirectUrl",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Event Redirect",
|
||||||
|
value: "eventTypeRedirectUrl",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const { data: eventTypesByGroup } = trpc.useQuery(["viewer.eventTypes"]);
|
||||||
|
|
||||||
|
const eventOptions: { label: string; value: string }[] = [];
|
||||||
|
eventTypesByGroup?.eventTypeGroups.forEach((group) => {
|
||||||
|
group.eventTypes.forEach((eventType) => {
|
||||||
|
const uniqueSlug = `${group.profile.slug}/${eventType.slug}`;
|
||||||
|
eventOptions.push({
|
||||||
|
label: uniqueSlug,
|
||||||
|
value: uniqueSlug,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = (route: Route, immutableTree: ImmutableTree, config: QueryBuilderUpdatedConfig) => {
|
||||||
|
const jsonTree = QbUtils.getTree(immutableTree);
|
||||||
|
setRoute(route.id, {
|
||||||
|
state: { tree: immutableTree, config: config },
|
||||||
|
queryValue: jsonTree,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderBuilder = useCallback(
|
||||||
|
(props: BuilderProps) => (
|
||||||
|
<div className="query-builder-container">
|
||||||
|
<div className="query-builder qb-lite">
|
||||||
|
<Builder {...props} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group mb-4 flex w-full flex-row items-center justify-between hover:bg-neutral-50 ltr:mr-2 rtl:ml-2">
|
||||||
|
{!route.isFallback ? (
|
||||||
|
<>
|
||||||
|
{moveUp?.check() ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="invisible absolute left-1/2 -mt-4 mb-4 -ml-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:left-[19px] sm:ml-0 sm:block"
|
||||||
|
onClick={() => moveUp?.fn()}>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{moveDown?.check() ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="invisible absolute left-1/2 mt-8 -ml-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:left-[19px] sm:ml-0 sm:block"
|
||||||
|
onClick={() => moveDown?.fn()}>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<div className="-mx-4 mb-4 flex w-full items-center rounded-sm border border-neutral-200 bg-white sm:mx-0 xl:px-8">
|
||||||
|
<div className="cal-query-builder m-4 my-8 w-full ">
|
||||||
|
<div>
|
||||||
|
<div className="flex w-full items-center text-sm text-gray-900">
|
||||||
|
<div className="flex flex-grow-0 whitespace-nowrap">
|
||||||
|
<Label>{route.isFallback ? "Fallback Route" : `Route ${index + 1}`}</Label>
|
||||||
|
<span>: Send Booker to</span>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
className="block w-full flex-grow px-2"
|
||||||
|
required
|
||||||
|
value={RoutingPages.find((page) => page.value === route.action.type)}
|
||||||
|
onChange={(item) => {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const action: Route["action"] = {
|
||||||
|
type: item.value,
|
||||||
|
value: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action.type === "customPageMessage") {
|
||||||
|
action.value = "We are not ready for you yet :(";
|
||||||
|
} else {
|
||||||
|
action.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoute(route.id, { action });
|
||||||
|
}}
|
||||||
|
options={RoutingPages}
|
||||||
|
/>
|
||||||
|
{route.action.type ? (
|
||||||
|
route.action.type === "customPageMessage" ? (
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
className="flex w-full flex-grow border-gray-300"
|
||||||
|
value={route.action.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : route.action.type === "externalRedirectUrl" ? (
|
||||||
|
<input
|
||||||
|
className="flex w-full flex-grow border-gray-300 text-sm"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={route.action.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||||
|
}}
|
||||||
|
placeholder="Enter External Redirect URL"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="block w-full">
|
||||||
|
<Select
|
||||||
|
required
|
||||||
|
options={eventOptions}
|
||||||
|
onChange={(option) => {
|
||||||
|
if (!option) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRoute(route.id, { action: { ...route.action, value: option.value } });
|
||||||
|
}}
|
||||||
|
value={eventOptions.find((eventOption) => eventOption.value === route.action.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{routes.length !== 1 && !route.isFallback ? (
|
||||||
|
<button className="ml-5" type="button">
|
||||||
|
<TrashIcon
|
||||||
|
className="m-0 h-4 w-4 text-neutral-500"
|
||||||
|
onClick={() => {
|
||||||
|
const newRoutes = routes.filter((r) => r.id !== route.id);
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<hr className="my-6 text-gray-200" />
|
||||||
|
<Query
|
||||||
|
{...config}
|
||||||
|
value={route.state.tree}
|
||||||
|
onChange={(immutableTree, config) => {
|
||||||
|
onChange(route, immutableTree, config as QueryBuilderUpdatedConfig);
|
||||||
|
}}
|
||||||
|
renderBuilder={renderBuilder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deserializeRoute = (route: SerializableRoute, config: QueryBuilderUpdatedConfig): Route => {
|
||||||
|
return {
|
||||||
|
...route,
|
||||||
|
state: {
|
||||||
|
tree: QbUtils.checkTree(QbUtils.loadTree(route.queryValue), config),
|
||||||
|
config: config,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Routes = ({
|
||||||
|
form,
|
||||||
|
appUrl,
|
||||||
|
}: {
|
||||||
|
form: inferSSRProps<typeof getServerSideProps>["form"];
|
||||||
|
appUrl: string;
|
||||||
|
}) => {
|
||||||
|
const { routes: serializedRoutes } = form;
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const config = getQueryBuilderConfig(form);
|
||||||
|
const [routes, setRoutes] = useState(() => {
|
||||||
|
const transformRoutes = () => {
|
||||||
|
const _routes = serializedRoutes || [getEmptyRoute()];
|
||||||
|
_routes.forEach((r) => {
|
||||||
|
if (!r.queryValue?.id) {
|
||||||
|
r.queryValue = { id: QbUtils.uuid(), type: "group" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return _routes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return transformRoutes().map((route) => deserializeRoute(route, config));
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutation = trpc.useMutation("viewer.app_routing_forms.form", {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast("Form routes saved successfully.", "success");
|
||||||
|
router.replace(router.asPath);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast("Something went wrong", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mainRoutes = routes.filter((route) => !route.isFallback);
|
||||||
|
let fallbackRoute = routes.find((route) => route.isFallback);
|
||||||
|
if (!fallbackRoute) {
|
||||||
|
fallbackRoute = deserializeRoute(createFallbackRoute(), config);
|
||||||
|
setRoutes((routes) => {
|
||||||
|
// Even though it's obvious that fallbackRoute is defined here but TypeScript just can't figure it out.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return [...routes, fallbackRoute!];
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
} else if (routes.indexOf(fallbackRoute) !== routes.length - 1) {
|
||||||
|
// Ensure fallback is last
|
||||||
|
setRoutes((routes) => {
|
||||||
|
// Even though it's obvious that fallbackRoute is defined here but TypeScript just can't figure it out.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return [...routes.filter((route) => route.id !== fallbackRoute!.id), fallbackRoute!];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const setRoute = (id: string, route: Partial<Route>) => {
|
||||||
|
const index = routes.findIndex((route) => route.id === id);
|
||||||
|
const newRoutes = [...routes];
|
||||||
|
newRoutes[index] = { ...routes[index], ...route };
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const swap = (from: number, to: number) => {
|
||||||
|
setRoutes((routes) => {
|
||||||
|
const newRoutes = [...routes];
|
||||||
|
const routeToSwap = newRoutes[from];
|
||||||
|
newRoutes[from] = newRoutes[to];
|
||||||
|
newRoutes[to] = routeToSwap;
|
||||||
|
return newRoutes;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col-reverse md:flex-row">
|
||||||
|
<form
|
||||||
|
className="w-full max-w-4xl ltr:mr-2 rtl:ml-2 md:w-9/12"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
const serializedRoutes: SerializableRoute[] = routes.map((route) => ({
|
||||||
|
id: route.id,
|
||||||
|
action: route.action,
|
||||||
|
isFallback: route.isFallback,
|
||||||
|
queryValue: route.queryValue,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updatedForm = {
|
||||||
|
...form,
|
||||||
|
routes: serializedRoutes,
|
||||||
|
};
|
||||||
|
mutation.mutate(updatedForm);
|
||||||
|
e.preventDefault();
|
||||||
|
}}>
|
||||||
|
{mainRoutes.map((route, key) => {
|
||||||
|
const jsonLogicQuery = QbUtils.jsonLogicFormat(route.state.tree, route.state.config);
|
||||||
|
console.log(`Route: ${JSON.stringify({ action: route.action, jsonLogicQuery })}`);
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={key}
|
||||||
|
config={config}
|
||||||
|
route={route}
|
||||||
|
moveUp={{
|
||||||
|
check: () => key !== 0,
|
||||||
|
fn: () => {
|
||||||
|
swap(key, key - 1);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
moveDown={{
|
||||||
|
// routes.length - 1 is fallback route always. So, routes.length - 2 is the last item that can be moved down
|
||||||
|
check: () => key !== routes.length - 2,
|
||||||
|
fn: () => {
|
||||||
|
swap(key, key + 1);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
routes={routes}
|
||||||
|
setRoute={setRoute}
|
||||||
|
setRoutes={setRoutes}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mb-8"
|
||||||
|
color="secondary"
|
||||||
|
StartIcon={PlusIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const newEmptyRoute = getEmptyRoute();
|
||||||
|
const newRoutes = [
|
||||||
|
...routes,
|
||||||
|
{
|
||||||
|
...newEmptyRoute,
|
||||||
|
state: {
|
||||||
|
tree: QbUtils.checkTree(QbUtils.loadTree(newEmptyRoute.queryValue), config),
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
}}>
|
||||||
|
Add New Route
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<Route
|
||||||
|
config={config}
|
||||||
|
route={fallbackRoute}
|
||||||
|
routes={routes}
|
||||||
|
setRoute={setRoute}
|
||||||
|
setRoutes={setRoutes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
|
<Button href="/apps/routing_forms/forms" color="secondary" tabIndex={-1}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={mutation.isLoading}>
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<SideBar form={form} appUrl={appUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RouteBuilder({
|
||||||
|
form,
|
||||||
|
appUrl,
|
||||||
|
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) {
|
||||||
|
return (
|
||||||
|
<RoutingShell appUrl={appUrl} heading={<PencilEdit value={form?.name} readOnly={true} />} form={form}>
|
||||||
|
<div className="route-config">
|
||||||
|
<Routes form={form} appUrl={appUrl} />
|
||||||
|
</div>
|
||||||
|
</RoutingShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async function getServerSideProps(
|
||||||
|
context: AppGetServerSidePropsContext,
|
||||||
|
prisma: AppPrisma,
|
||||||
|
user: AppUser
|
||||||
|
) {
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/auth/login",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { params } = context;
|
||||||
|
if (!params) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const formId = params.appPages[0];
|
||||||
|
if (!formId || params.appPages.length > 1) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||||
|
where: {
|
||||||
|
id: formId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!form) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
form: getSerializableForm(form),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,266 @@
|
||||||
|
import jsonLogic from "json-logic-js";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState, useRef, FormEvent } from "react";
|
||||||
|
import { Utils as QbUtils } from "react-awesome-query-builder";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
|
import { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetServerSideProps";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { Button } from "@calcom/ui";
|
||||||
|
import { trpc } from "@calcom/web/lib/trpc";
|
||||||
|
|
||||||
|
import { getSerializableForm } from "../../utils";
|
||||||
|
import { getQueryBuilderConfig } from "../route-builder/[...appPages]";
|
||||||
|
|
||||||
|
export type Response = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
value: string | string[];
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
type Form = inferSSRProps<typeof getServerSideProps>["form"];
|
||||||
|
|
||||||
|
type Route = NonNullable<Form["routes"]>[0];
|
||||||
|
|
||||||
|
function RoutingForm({ form }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const [customPageMessage, setCustomPageMessage] = useState<Route["action"]["value"]>("");
|
||||||
|
const formFillerIdRef = useRef(uuidv4());
|
||||||
|
|
||||||
|
// TODO: We might want to prevent spam from a single user by having same formFillerId across pageviews
|
||||||
|
// But technically, a user can fill form multiple times due to any number of reasons and we currently can't differentiate b/w that.
|
||||||
|
// - like a network error
|
||||||
|
// - or he abandoned booking flow in between
|
||||||
|
const formFillerId = formFillerIdRef.current;
|
||||||
|
const decidedActionRef = useRef<Route["action"]>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSubmit = (response: Response) => {
|
||||||
|
const decidedAction = processRoute({ form, response });
|
||||||
|
|
||||||
|
if (!decidedAction) {
|
||||||
|
// FIXME: Make sure that when a form is created, there is always a fallback route and then remove this.
|
||||||
|
alert("Define atleast 1 route");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseMutation.mutate({
|
||||||
|
formId: form.id,
|
||||||
|
formFillerId,
|
||||||
|
response: response,
|
||||||
|
});
|
||||||
|
decidedActionRef.current = decidedAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseMutation = trpc.useMutation("viewer.app_routing_forms.response", {
|
||||||
|
onSuccess: () => {
|
||||||
|
const decidedAction = decidedActionRef.current;
|
||||||
|
if (!decidedAction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Maybe take action after successful mutation
|
||||||
|
if (decidedAction.type === "customPageMessage") {
|
||||||
|
setCustomPageMessage(decidedAction.value);
|
||||||
|
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
||||||
|
router.push(`/${decidedAction.value}`);
|
||||||
|
} else if (decidedAction.type === "externalRedirectUrl") {
|
||||||
|
window.location.href = decidedAction.value;
|
||||||
|
}
|
||||||
|
showToast("Form submitted successfully! Redirecting now ...", "success");
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (e?.message) {
|
||||||
|
return void showToast(e?.message, "error");
|
||||||
|
}
|
||||||
|
if (e?.data?.code === "CONFLICT") {
|
||||||
|
return void showToast("Form already submitted", "error");
|
||||||
|
}
|
||||||
|
showToast("Something went wrong", "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [response, setResponse] = useState<Response>({});
|
||||||
|
|
||||||
|
const queryBuilderConfig = getQueryBuilderConfig(form);
|
||||||
|
|
||||||
|
const handleOnSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
return !customPageMessage ? (
|
||||||
|
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
||||||
|
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||||
|
<div className="mx-0 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:-mx-4 sm:px-8">
|
||||||
|
<Toaster position="bottom-right" />
|
||||||
|
|
||||||
|
<form onSubmit={handleOnSubmit}>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
|
||||||
|
{form.name}
|
||||||
|
</h1>
|
||||||
|
{form.description ? (
|
||||||
|
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{form.description}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{form.fields?.map((field) => {
|
||||||
|
const widget = queryBuilderConfig.widgets[field.type];
|
||||||
|
if (!("factory" in widget)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const Component = widget.factory;
|
||||||
|
|
||||||
|
const optionValues = field.selectText?.trim().split("\n");
|
||||||
|
const options = optionValues?.map((value) => {
|
||||||
|
const title = value;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="mb-4 block flex-col sm:flex ">
|
||||||
|
<div className="min-w-48 mb-2 flex-grow">
|
||||||
|
<label
|
||||||
|
id="slug-label"
|
||||||
|
htmlFor="slug"
|
||||||
|
className="flex text-sm font-medium text-neutral-700">
|
||||||
|
{field.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex rounded-sm shadow-sm">
|
||||||
|
<Component
|
||||||
|
value={response[field.id]?.value}
|
||||||
|
// required property isn't accepted by query-builder types
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/* @ts-ignore */
|
||||||
|
required={!!field.required}
|
||||||
|
listValues={options}
|
||||||
|
setValue={(value) => {
|
||||||
|
setResponse((response) => {
|
||||||
|
response = response || {};
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
[field.id]: {
|
||||||
|
label: field.label,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
|
<Button
|
||||||
|
loading={responseMutation.isLoading}
|
||||||
|
type="submit"
|
||||||
|
className="dark:text-darkmodebrandcontrast text-brandcontrast bg-brand dark:bg-darkmodebrand relative inline-flex items-center rounded-sm border border-transparent px-3 py-2 text-sm font-medium hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
||||||
|
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||||
|
<div className="-mx-4 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||||
|
<div>{customPageMessage}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processRoute({ form, response }: { form: Form; response: Response }) {
|
||||||
|
const queryBuilderConfig = getQueryBuilderConfig(form);
|
||||||
|
|
||||||
|
const routes = form.routes || [];
|
||||||
|
|
||||||
|
let decidedAction: Route["action"] | null = null;
|
||||||
|
|
||||||
|
const fallbackRoute = routes.find((route) => route.isFallback);
|
||||||
|
|
||||||
|
if (!fallbackRoute) {
|
||||||
|
throw new Error("Fallback route is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reorderedRoutes = routes.filter((route) => !route.isFallback).concat([fallbackRoute]);
|
||||||
|
|
||||||
|
reorderedRoutes.some((route) => {
|
||||||
|
if (!route) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const state = {
|
||||||
|
tree: QbUtils.checkTree(QbUtils.loadTree(route.queryValue), queryBuilderConfig),
|
||||||
|
config: queryBuilderConfig,
|
||||||
|
};
|
||||||
|
const jsonLogicQuery = QbUtils.jsonLogicFormat(state.tree, state.config);
|
||||||
|
const logic = jsonLogicQuery.logic;
|
||||||
|
let result = false;
|
||||||
|
const responseValues: Record<string, string | string[]> = {};
|
||||||
|
for (const [uuid, { value }] of Object.entries(response)) {
|
||||||
|
responseValues[uuid] = value;
|
||||||
|
}
|
||||||
|
if (logic) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
result = jsonLogic.apply(logic as any, responseValues);
|
||||||
|
} else {
|
||||||
|
// If no logic is provided, then consider it a match
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
decidedAction = route.action;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return decidedAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RoutingLink({ form }: { form: Form }) {
|
||||||
|
return <RoutingForm form={form} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async function getServerSideProps(
|
||||||
|
context: AppGetServerSidePropsContext,
|
||||||
|
prisma: AppPrisma
|
||||||
|
) {
|
||||||
|
const { params } = context;
|
||||||
|
if (!params) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const formId = params.appPages[0];
|
||||||
|
if (!formId || params.appPages.length > 1) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||||
|
where: {
|
||||||
|
id: formId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!form || form.disabled) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
form: getSerializableForm(form),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Page, chromium } from "@playwright/test";
|
||||||
|
|
||||||
|
// TODO: Import it in _playwright/config/globalSetup.ts and export it from there.
|
||||||
|
import { loginAsUser } from "@calcom/app-store/_apps-playwright/config/globalSetup";
|
||||||
|
import { hashPassword } from "@calcom/lib/auth";
|
||||||
|
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
async function installApp(appName: string, redirectUrl: string, page: Page) {
|
||||||
|
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/apps/${appName}`);
|
||||||
|
await page.click('[data-testid="install-app-button"]');
|
||||||
|
await page.waitForNavigation({
|
||||||
|
url: (url) => {
|
||||||
|
return url.pathname == redirectUrl;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser(userName: string) {
|
||||||
|
const email = `${userName}@example.com`;
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
username: userName,
|
||||||
|
email,
|
||||||
|
completedOnboarding: true,
|
||||||
|
password: await hashPassword(userName),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function globalSetup(/* config: FullConfig */) {
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: true,
|
||||||
|
});
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const appName = "routing_forms";
|
||||||
|
const userName = `${appName}-e2e-${Math.random()}`;
|
||||||
|
process.env.APP_USER_NAME = userName;
|
||||||
|
await createUser(userName);
|
||||||
|
await loginAsUser(userName, page);
|
||||||
|
await installApp(appName, `/apps/${appName}/forms`, page);
|
||||||
|
page.context().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalSetup;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import prisma from "@lib/prisma";
|
||||||
|
|
||||||
|
async function deleteUser(userName: string) {
|
||||||
|
await prisma.user.deleteMany({
|
||||||
|
where: {
|
||||||
|
AND: {
|
||||||
|
username: {
|
||||||
|
contains: userName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function globalTeardown(/* config: FullConfig */) {
|
||||||
|
await deleteUser("routing_forms-e2e");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default globalTeardown;
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { expect, Config } from "@playwright/test";
|
||||||
|
|
||||||
|
import { config as baseConfig } from "@calcom/app-store/_apps-playwright/config/playwright.config";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
...baseConfig,
|
||||||
|
globalSetup: require.resolve("./globalSetup"),
|
||||||
|
globalTeardown: require.resolve("./globalTeardown"),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect.extend({});
|
||||||
|
export default config;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { test as base } from "@playwright/test";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface Fixtures {}
|
||||||
|
export const test = base.extend<Fixtures>({});
|
|
@ -0,0 +1,12 @@
|
||||||
|
import prisma from "@calcom/web/lib/prisma";
|
||||||
|
|
||||||
|
export * from "@calcom/app-store/_apps-playwright/lib/testUtils";
|
||||||
|
export async function cleanUpForms() {
|
||||||
|
await prisma.app_RoutingForms_Form.deleteMany({
|
||||||
|
where: {
|
||||||
|
user: {
|
||||||
|
username: process.env.APP_USER_NAME,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test } from "../fixtures/fixtures";
|
||||||
|
import { cleanUpForms, todo } from "../lib/testUtils";
|
||||||
|
|
||||||
|
async function addForm(page: Page) {
|
||||||
|
await page.click('[data-testid="new-routing-form"]');
|
||||||
|
await page.waitForSelector('[data-testid="add-attribute"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifySelectOptions(
|
||||||
|
selector: { selector: string; nth: number },
|
||||||
|
expectedOptions: string[],
|
||||||
|
page: Page
|
||||||
|
) {
|
||||||
|
await page.locator(selector.selector).nth(selector.nth).click();
|
||||||
|
const selectOptions = await page
|
||||||
|
.locator(selector.selector)
|
||||||
|
.nth(selector.nth)
|
||||||
|
.locator('[id*="react-select-"][aria-disabled]')
|
||||||
|
.allInnerTexts();
|
||||||
|
const sortedSelectOptions = [...selectOptions].sort();
|
||||||
|
const sortedExpectedOptions = [...expectedOptions].sort();
|
||||||
|
expect(sortedSelectOptions).toEqual(sortedExpectedOptions);
|
||||||
|
return {
|
||||||
|
optionsInUi: selectOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillForm(
|
||||||
|
page: Page,
|
||||||
|
form: { description: string; field?: { typeIndex: number; label: string } }
|
||||||
|
) {
|
||||||
|
await page.click('[data-testid="add-attribute"]');
|
||||||
|
await page.fill('[data-testid="description"]', form.description);
|
||||||
|
|
||||||
|
// Verify all Options of SelectBox
|
||||||
|
const { optionsInUi: types } = await verifySelectOptions(
|
||||||
|
{ selector: ".data-testid-attribute-type", nth: 0 },
|
||||||
|
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
|
||||||
|
page
|
||||||
|
);
|
||||||
|
|
||||||
|
if (form.field) {
|
||||||
|
await page.fill('[name="fields.0.label"]', form.field.label);
|
||||||
|
await page.click(".data-testid-attribute-type");
|
||||||
|
await page.locator('[id*="react-select-"][aria-disabled]').nth(form.field.typeIndex).click();
|
||||||
|
}
|
||||||
|
await page.click('[data-testid="update-form"]');
|
||||||
|
await page.waitForSelector(".data-testid-toast-success");
|
||||||
|
return {
|
||||||
|
types,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test.use({ storageState: `playwright/artifacts/${process.env.APP_USER_NAME}StorageState.json` });
|
||||||
|
test.describe("Forms", () => {
|
||||||
|
test("should be able to add a new form and see it in forms list", async ({ page }) => {
|
||||||
|
page.goto("/");
|
||||||
|
|
||||||
|
await page.click('[href="/apps/routing_forms/forms"]');
|
||||||
|
await page.waitForSelector('[data-testid="empty-screen"]');
|
||||||
|
|
||||||
|
await addForm(page);
|
||||||
|
|
||||||
|
await page.click('[href="/apps/routing_forms/forms"]');
|
||||||
|
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
||||||
|
expect(await page.locator('[data-testid="routing-forms-list"] > li').count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be able to edit the form", async ({ page }) => {
|
||||||
|
await page.goto("/apps/routing_forms/forms");
|
||||||
|
|
||||||
|
await addForm(page);
|
||||||
|
const description = "Test Description";
|
||||||
|
|
||||||
|
const field = {
|
||||||
|
label: "Test Label",
|
||||||
|
typeIndex: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { types } = await fillForm(page, {
|
||||||
|
description,
|
||||||
|
field: field,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
expect(await page.inputValue(`[data-testid="description"]`), description);
|
||||||
|
expect(await page.locator('[data-testid="attribute"]').count()).toBe(1);
|
||||||
|
expect(await page.inputValue('[name="fields.0.label"]')).toBe(field.label);
|
||||||
|
expect(await page.locator(".data-testid-attribute-type").first().innerText()).toBe(
|
||||||
|
types[field.typeIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click('[href*="/apps/routing_forms/route-builder/"]');
|
||||||
|
await page.click('[data-testid="add-rule"]');
|
||||||
|
await verifySelectOptions(
|
||||||
|
{
|
||||||
|
selector: ".rule-container .data-testid-field-select",
|
||||||
|
nth: 0,
|
||||||
|
},
|
||||||
|
[field.label],
|
||||||
|
page
|
||||||
|
);
|
||||||
|
});
|
||||||
|
todo("Test Routing Link");
|
||||||
|
test.afterAll(() => {
|
||||||
|
cleanUpForms();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 265 B |
|
@ -0,0 +1,182 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createProtectedRouter } from "@server/createRouter";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import { zodFields, zodRoutes } from "./zod";
|
||||||
|
|
||||||
|
const app_RoutingForms = createProtectedRouter()
|
||||||
|
.query("forms", {
|
||||||
|
async resolve({ ctx: { user, prisma } }) {
|
||||||
|
return await prisma.app_RoutingForms_Form.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.query("form", {
|
||||||
|
input: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx: { prisma }, input }) {
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||||
|
where: {
|
||||||
|
id: input.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return form;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("form", {
|
||||||
|
input: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
|
fields: zodFields,
|
||||||
|
routes: zodRoutes,
|
||||||
|
}),
|
||||||
|
async resolve({ ctx: { user, prisma }, input }) {
|
||||||
|
const { name, id, routes, description, disabled } = input;
|
||||||
|
let { fields } = input;
|
||||||
|
fields = fields || [];
|
||||||
|
return await prisma.app_RoutingForms_Form.upsert({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: fields,
|
||||||
|
name: name,
|
||||||
|
description,
|
||||||
|
// Prisma doesn't allow setting null value directly for JSON. It recommends using JsonNull for that case.
|
||||||
|
routes: routes === null ? Prisma.JsonNull : routes,
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
disabled: disabled,
|
||||||
|
fields: fields,
|
||||||
|
name: name,
|
||||||
|
description,
|
||||||
|
routes: routes === null ? Prisma.JsonNull : routes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// TODO: Can't se use DELETE method on form?
|
||||||
|
.mutation("deleteForm", {
|
||||||
|
input: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
return await ctx.prisma.app_RoutingForms_Form.delete({
|
||||||
|
where: {
|
||||||
|
id: input.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mutation("response", {
|
||||||
|
input: z.object({
|
||||||
|
formId: z.string(),
|
||||||
|
formFillerId: z.string(),
|
||||||
|
response: z.record(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
value: z.union([z.string(), z.array(z.string())]),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx: { prisma }, input }) {
|
||||||
|
try {
|
||||||
|
const { response, formId } = input;
|
||||||
|
const form = await prisma.app_RoutingForms_Form.findFirst({
|
||||||
|
where: {
|
||||||
|
id: formId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!form) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const fieldsParsed = zodFields.safeParse(form.fields);
|
||||||
|
if (!fieldsParsed.success) {
|
||||||
|
// This should not be possible normally as before saving the form it is verified by zod
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = fieldsParsed.data;
|
||||||
|
|
||||||
|
if (!fields) {
|
||||||
|
// There is no point in submitting a form that doesn't have fields defined
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingFields = fields
|
||||||
|
.filter((field) => !(field.required ? response[field.id]?.value : true))
|
||||||
|
.map((f) => f.label);
|
||||||
|
|
||||||
|
if (missingFields.length) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Missing required fields ${missingFields.join(", ")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const invalidFields = fields
|
||||||
|
.filter((field) => {
|
||||||
|
const fieldValue = response[field.id]?.value;
|
||||||
|
// The field isn't required at this point. Validate only if it's set
|
||||||
|
if (!fieldValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let schema;
|
||||||
|
if (field.type === "email") {
|
||||||
|
schema = z.string().email();
|
||||||
|
} else if (field.type === "phone") {
|
||||||
|
schema = z.any();
|
||||||
|
} else {
|
||||||
|
schema = z.any();
|
||||||
|
}
|
||||||
|
return !schema.safeParse(fieldValue).success;
|
||||||
|
})
|
||||||
|
.map((f) => ({ label: f.label, type: f.type }));
|
||||||
|
|
||||||
|
if (invalidFields.length) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: `Invalid fields ${invalidFields.map((f) => `${f.label}: ${f.type}`)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await prisma.app_RoutingForms_FormResponse.create({
|
||||||
|
data: input,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === "P2002") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "CONFLICT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app_RoutingForms;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { App_RoutingForms_Form } from "@prisma/client";
|
||||||
|
|
||||||
|
import { zodFields, zodRoutes } from "./zod";
|
||||||
|
|
||||||
|
export function getSerializableForm<TForm extends App_RoutingForms_Form>(form: TForm) {
|
||||||
|
const routesParsed = zodRoutes.safeParse(form.routes);
|
||||||
|
if (!routesParsed.success) {
|
||||||
|
throw new Error("Error parsing routes");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsParsed = zodFields.safeParse(form.fields);
|
||||||
|
if (!fieldsParsed.success) {
|
||||||
|
throw new Error("Error parsing fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ideally we shouldb't have needed to explicitly type it but due to some reason it's not working reliably with VSCode TypeCheck
|
||||||
|
const serializableForm: Omit<TForm, "fields" | "routes" | "createdAt" | "updatedAt"> & {
|
||||||
|
fields: typeof fieldsParsed["data"];
|
||||||
|
routes: typeof routesParsed["data"];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
} = {
|
||||||
|
...form,
|
||||||
|
fields: fieldsParsed.data,
|
||||||
|
routes: routesParsed.data,
|
||||||
|
createdAt: form.createdAt.toString(),
|
||||||
|
updatedAt: form.updatedAt.toString(),
|
||||||
|
};
|
||||||
|
return serializableForm;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const zodFields = z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
selectText: z.string().optional(),
|
||||||
|
required: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional();
|
||||||
|
export const zodRoutes = z
|
||||||
|
.union([
|
||||||
|
z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
queryValue: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
type: z.union([z.literal("group"), z.literal("switch_group")]),
|
||||||
|
children1: z.any(),
|
||||||
|
properties: z.any(),
|
||||||
|
}),
|
||||||
|
isFallback: z.boolean().optional(),
|
||||||
|
action: z.object({
|
||||||
|
// TODO: Make it a union type of "customPageMessage" and ..
|
||||||
|
type: z.union([
|
||||||
|
z.literal("customPageMessage"),
|
||||||
|
z.literal("externalRedirectUrl"),
|
||||||
|
z.literal("eventTypeRedirectUrl"),
|
||||||
|
]),
|
||||||
|
value: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
z.null(),
|
||||||
|
])
|
||||||
|
.optional();
|
|
@ -5,6 +5,8 @@ import "./next-auth";
|
||||||
|
|
||||||
export declare module "next" {
|
export declare module "next" {
|
||||||
interface NextApiRequest extends IncomingMessage {
|
interface NextApiRequest extends IncomingMessage {
|
||||||
|
// args is defined by /integrations/[...args] endpoint
|
||||||
|
query: Partial<{ [key: string]: string | string[] }> & { args: string[] };
|
||||||
session?: Session | null;
|
session?: Session | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
// TODO: Generate this file automatically. This supposed to contain trpc-routes from all the apps automatically imported here and then exported.
|
||||||
|
// Can't use this file right now as I am not able to figure out how to keep getting tRPC typesafety with merge calls done on already created router
|
||||||
|
// Till that time import routers from each app directly to core.
|
||||||
|
// import { Router } from "@trpc/server/dist/declarations/src/router";
|
|
@ -1,8 +1,24 @@
|
||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/react-library.json",
|
"extends": "@calcom/tsconfig/react-library.json",
|
||||||
"include": [".", "@calcom/types"],
|
|
||||||
"exclude": ["dist", "build", "node_modules"],
|
"exclude": ["dist", "build", "node_modules"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@components/*": ["../../apps/web/components/*"],
|
||||||
|
"@lib/*": ["../../apps/web/lib/*"],
|
||||||
|
"@server/*": ["../../apps/web/server/*"],
|
||||||
|
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||||
|
},
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
}
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"@calcom/types",
|
||||||
|
"../../packages/types/*.d.ts",
|
||||||
|
"../../packages/types/next-auth.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"../../apps/web/server/**/*.ts",
|
||||||
|
"../../apps/web/server/**/*.tsx"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,12 @@ export type IntegrationOAuthCallbackState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface InstallAppButtonProps {
|
export interface InstallAppButtonProps {
|
||||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
render: (
|
||||||
|
renderProps:
|
||||||
|
| ButtonBaseProps & {
|
||||||
|
/** Tells that the default render component should be used */
|
||||||
|
useDefaultComponent?: boolean;
|
||||||
|
}
|
||||||
|
) => JSX.Element;
|
||||||
onChanged?: () => unknown;
|
onChanged?: () => unknown;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export default function showToast(message: string, variant: "success" | "warning
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
},
|
},
|
||||||
|
className: "data-testid-toast-success",
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "App_RoutingForms_Form" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"routes" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"fields" JSONB,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"disabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
CONSTRAINT "App_RoutingForms_Form_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "App_RoutingForms_FormResponse" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"formFillerId" TEXT NOT NULL,
|
||||||
|
"formId" TEXT NOT NULL,
|
||||||
|
"response" JSONB NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "App_RoutingForms_FormResponse_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "App_RoutingForms_FormResponse_formFillerId_formId_key" ON "App_RoutingForms_FormResponse"("formFillerId", "formId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "App_RoutingForms_Form" ADD CONSTRAINT "App_RoutingForms_Form_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "App_RoutingForms_FormResponse" ADD CONSTRAINT "App_RoutingForms_FormResponse_formId_fkey" FOREIGN KEY ("formId") REFERENCES "App_RoutingForms_Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -181,6 +181,8 @@ model User {
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
workflows Workflow[]
|
workflows Workflow[]
|
||||||
|
routingForms App_RoutingForms_Form[] @relation("routing-form")
|
||||||
|
|
||||||
|
|
||||||
Feedback Feedback[]
|
Feedback Feedback[]
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
|
@ -488,6 +490,30 @@ model App {
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model App_RoutingForms_Form {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
description String?
|
||||||
|
routes Json?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
name String
|
||||||
|
fields Json?
|
||||||
|
user User @relation("routing-form", fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
|
responses App_RoutingForms_FormResponse[]
|
||||||
|
disabled Boolean @default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
model App_RoutingForms_FormResponse {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
formFillerId String @default(cuid())
|
||||||
|
form App_RoutingForms_Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||||
|
formId String
|
||||||
|
response Json
|
||||||
|
|
||||||
|
@@unique([formFillerId, formId])
|
||||||
|
}
|
||||||
|
|
||||||
model Feedback {
|
model Feedback {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
date DateTime
|
date DateTime
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"/*": "This file is auto-generated and managed by `yarn app-store`. Don't edit manually but it is to be committed",
|
"/*": "This file is auto-generated and managed by `yarn app-store`. Don't edit manually but it is to be committed",
|
||||||
|
"dirName": "routing_forms",
|
||||||
|
"categories": ["other"],
|
||||||
|
"slug": "routing_forms",
|
||||||
|
"type": "routing_forms_other"
|
||||||
|
},
|
||||||
|
{
|
||||||
"dirName": "whereby",
|
"dirName": "whereby",
|
||||||
"categories": ["video"],
|
"categories": ["video"],
|
||||||
"slug": "whereby",
|
"slug": "whereby",
|
||||||
|
|
|
@ -108,6 +108,7 @@ async function main() {
|
||||||
invite_link: process.env.ZAPIER_INVITE_LINK,
|
invite_link: process.env.ZAPIER_INVITE_LINK,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web3 apps
|
// Web3 apps
|
||||||
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
|
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
|
||||||
await createApp("metamask", "metamask", ["web3"], "metamask_web3");
|
await createApp("metamask", "metamask", ["web3"], "metamask_web3");
|
||||||
|
|
|
@ -73,4 +73,6 @@ export interface App {
|
||||||
price?: number;
|
price?: number;
|
||||||
/** only required for "usage-based" billing. % of commission for paid bookings */
|
/** only required for "usage-based" billing. % of commission for paid bookings */
|
||||||
commission?: number;
|
commission?: number;
|
||||||
|
licenseRequired?: boolean;
|
||||||
|
isProOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||||
|
import { CalendsoSessionUser } from "next-auth";
|
||||||
|
|
||||||
|
import prisma from "@calcom/web/lib/prisma";
|
||||||
|
|
||||||
|
export type AppUser = CalendsoSessionUser | undefined;
|
||||||
|
export type AppPrisma = typeof prisma;
|
||||||
|
export type AppGetServerSidePropsContext = GetServerSidePropsContext<{
|
||||||
|
appPages: string[];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type AppGetServerSideProps = (
|
||||||
|
context: AppGetServerSidePropsContext,
|
||||||
|
prisma: AppPrisma,
|
||||||
|
user: AppUser
|
||||||
|
) => GetServerSidePropsResult;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { NextApiHandler } from "next";
|
||||||
|
import { Session } from "next-auth";
|
||||||
|
|
||||||
|
import { Credential } from "@calcom/prisma/client";
|
||||||
|
|
||||||
|
export type AppDeclarativeHandler = {
|
||||||
|
appType: string;
|
||||||
|
slug: string;
|
||||||
|
supportsMultipleInstalls: false;
|
||||||
|
handlerType: "add";
|
||||||
|
createCredential: (arg: { user: Session["user"]; appType: string; slug: string }) => Promise<Credential>;
|
||||||
|
supportsMultipleInstalls: boolean;
|
||||||
|
redirectUrl: string;
|
||||||
|
};
|
||||||
|
export type AppHandler = AppDeclarativeHandler | NextApiHandler;
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Root as ToggleGroupPrimitive, Item as ToggleGroupItemPrimitive } from "@radix-ui/react-toggle-group";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
|
const boolean = (yesNo: "yes" | "no") => (yesNo === "yes" ? true : yesNo === "no" ? false : undefined);
|
||||||
|
const yesNo = (boolean?: boolean) => (boolean === true ? "yes" : boolean === false ? "no" : undefined);
|
||||||
|
|
||||||
|
export default function BooleanToggleGroup({
|
||||||
|
defaultValue = true,
|
||||||
|
value,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onValueChange = () => {},
|
||||||
|
}: {
|
||||||
|
defaultValue?: boolean;
|
||||||
|
value?: boolean;
|
||||||
|
onValueChange?: (value?: boolean) => void;
|
||||||
|
}) {
|
||||||
|
// Maintain a state because it is not necessary that onValueChange the parent component would re-render. Think react-hook-form
|
||||||
|
// Also maintain a string as boolean isn't accepted as ToggleGroupPrimitive value
|
||||||
|
const [yesNoValue, setYesNoValue] = useState<"yes" | "no" | undefined>(yesNo(value));
|
||||||
|
|
||||||
|
if (!yesNoValue) {
|
||||||
|
setYesNoValue(yesNo(defaultValue));
|
||||||
|
onValueChange(defaultValue);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive
|
||||||
|
value={yesNoValue}
|
||||||
|
type="single"
|
||||||
|
className="rounded-sm"
|
||||||
|
onValueChange={(yesNoValue: "yes" | "no") => {
|
||||||
|
setYesNoValue(yesNoValue);
|
||||||
|
onValueChange(boolean(yesNoValue));
|
||||||
|
}}>
|
||||||
|
<ToggleGroupItemPrimitive
|
||||||
|
className={classNames(
|
||||||
|
boolean(yesNoValue) ? "bg-gray-200" : "",
|
||||||
|
"border border-gray-300 py-2 px-3 text-sm"
|
||||||
|
)}
|
||||||
|
value="yes">
|
||||||
|
Yes
|
||||||
|
</ToggleGroupItemPrimitive>
|
||||||
|
<ToggleGroupItemPrimitive
|
||||||
|
className={classNames(
|
||||||
|
!boolean(yesNoValue) ? "bg-gray-200" : "",
|
||||||
|
"border border-l-0 border-gray-300 py-2 px-3 text-sm"
|
||||||
|
)}
|
||||||
|
value="no">
|
||||||
|
No
|
||||||
|
</ToggleGroupItemPrimitive>
|
||||||
|
</ToggleGroupPrimitive>
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,7 +13,9 @@ export default function EmptyScreen({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
|
<div
|
||||||
|
data-testid="empty-screen"
|
||||||
|
className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,3 +5,4 @@ export { default as Loader } from "./Loader";
|
||||||
export * from "./skeleton";
|
export * from "./skeleton";
|
||||||
export { default as Switch } from "./Switch";
|
export { default as Switch } from "./Switch";
|
||||||
export { default as Tooltip } from "./Tooltip";
|
export { default as Tooltip } from "./Tooltip";
|
||||||
|
export { default as BooleanToggleGroup } from "./BooleanToggleGroup";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { loadEnvConfig } from "@next/env";
|
||||||
import { Browser, chromium } from "@playwright/test";
|
import { Browser, chromium } from "@playwright/test";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
async function loginAsUser(username: string, browser: Browser) {
|
export async function loginAsUser(username: string, browser: Browser) {
|
||||||
// Skip is file exists
|
// Skip is file exists
|
||||||
if (fs.existsSync(`playwright/artifacts/${username}StorageState.json`)) return;
|
if (fs.existsSync(`playwright/artifacts/${username}StorageState.json`)) return;
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
|
|
|
@ -143,6 +143,7 @@
|
||||||
"outputs": ["../../../apps/web/public/embed/**"]
|
"outputs": ["../../../apps/web/public/embed/**"]
|
||||||
},
|
},
|
||||||
"embed-tests-update-snapshots:ci": {
|
"embed-tests-update-snapshots:ci": {
|
||||||
|
"cache": false,
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
"@calcom/prisma#db-seed",
|
"@calcom/prisma#db-seed",
|
||||||
"@calcom/web#build",
|
"@calcom/web#build",
|
||||||
|
@ -151,6 +152,10 @@
|
||||||
"^embed-tests-update-snapshots:ci"
|
"^embed-tests-update-snapshots:ci"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"app-e2e-quick": {
|
||||||
|
"cache": false,
|
||||||
|
"dependsOn": ["@calcom/prisma#db-seed", "@calcom/web#build", "^app-e2e-quick"]
|
||||||
|
},
|
||||||
"//#env-check:common": {
|
"//#env-check:common": {
|
||||||
"inputs": ["./.env.example", "./.env"],
|
"inputs": ["./.env.example", "./.env"],
|
||||||
"outputs": ["./.env"]
|
"outputs": ["./.env"]
|
||||||
|
|
375
yarn.lock
375
yarn.lock
|
@ -852,6 +852,18 @@
|
||||||
fast-equals "^1.6.3"
|
fast-equals "^1.6.3"
|
||||||
lodash "^4.17.15"
|
lodash "^4.17.15"
|
||||||
|
|
||||||
|
"@date-io/core@^1.3.13":
|
||||||
|
version "1.3.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa"
|
||||||
|
integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==
|
||||||
|
|
||||||
|
"@date-io/moment@^1.3.13":
|
||||||
|
version "1.3.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-1.3.13.tgz#56c2772bc4f6675fc6970257e6033e7a7c2960f0"
|
||||||
|
integrity sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==
|
||||||
|
dependencies:
|
||||||
|
"@date-io/core" "^1.3.13"
|
||||||
|
|
||||||
"@emotion/babel-plugin@^11.7.1":
|
"@emotion/babel-plugin@^11.7.1":
|
||||||
version "11.7.2"
|
version "11.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz#fec75f38a6ab5b304b0601c74e2a5e77c95e5fa0"
|
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz#fec75f38a6ab5b304b0601c74e2a5e77c95e5fa0"
|
||||||
|
@ -2157,11 +2169,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack-bundle-analyzer "4.3.0"
|
webpack-bundle-analyzer "4.3.0"
|
||||||
|
|
||||||
"@next/env@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.1.6.tgz#5f44823a78335355f00f1687cfc4f1dafa3eca08"
|
|
||||||
integrity sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA==
|
|
||||||
|
|
||||||
"@next/env@12.2.0":
|
"@next/env@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.0.tgz#17ce2d9f5532b677829840037e06f208b7eed66b"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.0.tgz#17ce2d9f5532b677829840037e06f208b7eed66b"
|
||||||
|
@ -2172,11 +2179,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.1.tgz#083cc88469931fc3dc32bb633623321c29971a09"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.1.tgz#083cc88469931fc3dc32bb633623321c29971a09"
|
||||||
integrity sha512-lz3TJKIvbdGRUsUr/+h3vy7XvBNGTGzHwhurk5AtqrABj4Zyo70xbshcI7YQTNUK4x9OA/E+SOcXvVx0DHmFRw==
|
integrity sha512-lz3TJKIvbdGRUsUr/+h3vy7XvBNGTGzHwhurk5AtqrABj4Zyo70xbshcI7YQTNUK4x9OA/E+SOcXvVx0DHmFRw==
|
||||||
|
|
||||||
"@next/env@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.2.2.tgz#cc1a0a445bd254499e30f632968c03192455f4cc"
|
|
||||||
integrity sha512-BqDwE4gDl1F608TpnNxZqrCn6g48MBjvmWFEmeX5wEXDXh3IkAOw6ASKUgjT8H4OUePYFqghDFUss5ZhnbOUjw==
|
|
||||||
|
|
||||||
"@next/eslint-plugin-next@12.1.6":
|
"@next/eslint-plugin-next@12.1.6":
|
||||||
version "12.1.6"
|
version "12.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
|
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-12.1.6.tgz#dde3f98831f15923b25244588d924c716956292e"
|
||||||
|
@ -2184,11 +2186,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz#79a35349b98f2f8c038ab6261aa9cd0d121c03f9"
|
|
||||||
integrity sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==
|
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@12.2.0":
|
"@next/swc-android-arm-eabi@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.0.tgz#f116756e668b267de84b76f068d267a12f18eb22"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.0.tgz#f116756e668b267de84b76f068d267a12f18eb22"
|
||||||
|
@ -2199,16 +2196,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.1.tgz#26a4363bd3857b934e7ad63aa1647d83b380ce1f"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.1.tgz#26a4363bd3857b934e7ad63aa1647d83b380ce1f"
|
||||||
integrity sha512-Gk7fvo1McA9gues9hixoeoxKnvvUusW0P+fya4ZAU3us+bQm1EqSoDrnOrUsdsgwIPQ3HobOJPY5C3xvKOl/tA==
|
integrity sha512-Gk7fvo1McA9gues9hixoeoxKnvvUusW0P+fya4ZAU3us+bQm1EqSoDrnOrUsdsgwIPQ3HobOJPY5C3xvKOl/tA==
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.2.2.tgz#f6c4111e6371f73af6bf80c9accb3d96850a92cd"
|
|
||||||
integrity sha512-VHjuCHeq9qCprUZbsRxxM/VqSW8MmsUtqB5nEpGEgUNnQi/BTm/2aK8tl7R4D0twGKRh6g1AAeFuWtXzk9Z/vQ==
|
|
||||||
|
|
||||||
"@next/swc-android-arm64@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz#ec08ea61794f8752c8ebcacbed0aafc5b9407456"
|
|
||||||
integrity sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==
|
|
||||||
|
|
||||||
"@next/swc-android-arm64@12.2.0":
|
"@next/swc-android-arm64@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.0.tgz#cbd9e329cef386271d4e746c08416b5d69342c24"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.0.tgz#cbd9e329cef386271d4e746c08416b5d69342c24"
|
||||||
|
@ -2219,16 +2206,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.1.tgz#28c7e964208e80d4b3ff791f323fbe425eae26fe"
|
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.1.tgz#28c7e964208e80d4b3ff791f323fbe425eae26fe"
|
||||||
integrity sha512-J+QwWRm2+bOtacZFahoplX3dCYGDpou86VjfcE+M5/E0UCtBmZ6JvItyV4scK8wSKHQQUWq8DmOEm/C0lhsSRQ==
|
integrity sha512-J+QwWRm2+bOtacZFahoplX3dCYGDpou86VjfcE+M5/E0UCtBmZ6JvItyV4scK8wSKHQQUWq8DmOEm/C0lhsSRQ==
|
||||||
|
|
||||||
"@next/swc-android-arm64@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.2.2.tgz#b69de59c51e631a7600439e7a8993d6e82f3369e"
|
|
||||||
integrity sha512-v5EYzXUOSv0r9mO/2PX6mOcF53k8ndlu9yeFHVAWW1Dhw2jaJcvTRcCAwYYN8Q3tDg0nH3NbEltJDLKmcJOuVA==
|
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz#d1053805615fd0706e9b1667893a72271cd87119"
|
|
||||||
integrity sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==
|
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@12.2.0":
|
"@next/swc-darwin-arm64@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.0.tgz#3473889157ba70b30ccdd4f59c46232d841744e2"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.0.tgz#3473889157ba70b30ccdd4f59c46232d841744e2"
|
||||||
|
@ -2239,16 +2216,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.1.tgz#ae68b105956c985214219d4f676b2e57c882d5ae"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.1.tgz#ae68b105956c985214219d4f676b2e57c882d5ae"
|
||||||
integrity sha512-teSfpKHdHQER4FVVCdvS0fHff35Gh4LB2DZ2eNAateIluP2Gnl+tT881MeM4Knvl2Mvm3Z3vtSJNthVoveJnMA==
|
integrity sha512-teSfpKHdHQER4FVVCdvS0fHff35Gh4LB2DZ2eNAateIluP2Gnl+tT881MeM4Knvl2Mvm3Z3vtSJNthVoveJnMA==
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.2.2.tgz#80157c91668eff95b72d052428c353eab0fc4c50"
|
|
||||||
integrity sha512-JCoGySHKGt+YBk7xRTFGx1QjrnCcwYxIo3yGepcOq64MoiocTM3yllQWeOAJU2/k9MH0+B5E9WUSme4rOCBbpA==
|
|
||||||
|
|
||||||
"@next/swc-darwin-x64@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz#2d1b926a22f4c5230d5b311f9c56cfdcc406afec"
|
|
||||||
integrity sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==
|
|
||||||
|
|
||||||
"@next/swc-darwin-x64@12.2.0":
|
"@next/swc-darwin-x64@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.0.tgz#b25198c3ef4c906000af49e4787a757965f760bb"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.0.tgz#b25198c3ef4c906000af49e4787a757965f760bb"
|
||||||
|
@ -2259,11 +2226,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.1.tgz#27da7988d01847b642b8d5c274f14bd82439fbb0"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.1.tgz#27da7988d01847b642b8d5c274f14bd82439fbb0"
|
||||||
integrity sha512-flA1H+9krrINtdWoXBzeESkdIV34OKX0+Lnqd90J1nsERTXntYy6CNOMxMtv1otAcnFy7EHYJQIL8URuu/2XXg==
|
integrity sha512-flA1H+9krrINtdWoXBzeESkdIV34OKX0+Lnqd90J1nsERTXntYy6CNOMxMtv1otAcnFy7EHYJQIL8URuu/2XXg==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.2.2.tgz#12be2f58e676fccff3d48a62921b9927ed295133"
|
|
||||||
integrity sha512-dztDtvfkhUqiqpXvrWVccfGhLe44yQ5tQ7B4tBfnsOR6vxzI9DNPHTlEOgRN9qDqTAcFyPxvg86mn4l8bB9Jcw==
|
|
||||||
|
|
||||||
"@next/swc-freebsd-x64@12.2.0":
|
"@next/swc-freebsd-x64@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.0.tgz#78e2213f8b703be0fef23a49507779b4a9842929"
|
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.0.tgz#78e2213f8b703be0fef23a49507779b4a9842929"
|
||||||
|
@ -2274,16 +2236,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.1.tgz#0b4cd5c1707218cac86a7a58e116c74998da6286"
|
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.1.tgz#0b4cd5c1707218cac86a7a58e116c74998da6286"
|
||||||
integrity sha512-SkAjp7B7aBxAsRVMZGiAp/qMkh65PLzYuLBTsBSu+4fxFuKF7MAEgaIUhvC8zzD58A+Y9yrY/3813bhtrwkcuA==
|
integrity sha512-SkAjp7B7aBxAsRVMZGiAp/qMkh65PLzYuLBTsBSu+4fxFuKF7MAEgaIUhvC8zzD58A+Y9yrY/3813bhtrwkcuA==
|
||||||
|
|
||||||
"@next/swc-freebsd-x64@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.2.2.tgz#de1363431a49059f1efb8c0f86ce6a79c53b3a95"
|
|
||||||
integrity sha512-JUnXB+2xfxqsAvhFLPJpU1NeyDsvJrKoOjpV7g3Dxbno2Riu4tDKn3kKF886yleAuD/1qNTUCpqubTvbbT2VoA==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz#c021918d2a94a17f823106a5e069335b8a19724f"
|
|
||||||
integrity sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@12.2.0":
|
"@next/swc-linux-arm-gnueabihf@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.0.tgz#80a4baf0ba699357e7420e2dea998908dcef5055"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.0.tgz#80a4baf0ba699357e7420e2dea998908dcef5055"
|
||||||
|
@ -2294,16 +2246,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.1.tgz#3b93a18f1264a88985bc3a01e0067aa1afe0ab72"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.1.tgz#3b93a18f1264a88985bc3a01e0067aa1afe0ab72"
|
||||||
integrity sha512-V7ov2LXrLWuYVH/syzrzpmwWumg5rCh0siwOPNCRzVkrpgP8WoIRNdeZ/NQIj0ng+kq7gDF1jib583Lk0wbDeQ==
|
integrity sha512-V7ov2LXrLWuYVH/syzrzpmwWumg5rCh0siwOPNCRzVkrpgP8WoIRNdeZ/NQIj0ng+kq7gDF1jib583Lk0wbDeQ==
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.2.2.tgz#d5b8e0d1bb55bbd9db4d2fec018217471dc8b9e6"
|
|
||||||
integrity sha512-XeYC/qqPLz58R4pjkb+x8sUUxuGLnx9QruC7/IGkK68yW4G17PHwKI/1njFYVfXTXUukpWjcfBuauWwxp9ke7Q==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz#ac55c07bfabde378dfa0ce2b8fc1c3b2897e81ae"
|
|
||||||
integrity sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@12.2.0":
|
"@next/swc-linux-arm64-gnu@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.0.tgz#134a42ddea804d6bf04761607f774432c3126de6"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.0.tgz#134a42ddea804d6bf04761607f774432c3126de6"
|
||||||
|
@ -2314,16 +2256,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.1.tgz#9887a772f96680afa440ac3e6f716fd20d7f4178"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.1.tgz#9887a772f96680afa440ac3e6f716fd20d7f4178"
|
||||||
integrity sha512-HlnDQD3r4YqCj2gu6uo86oEM0ixBsyKLaPcZcGwWAD5mFG5R4zzTZG7BO2wJkGWmkzijHluE14dlTmfzc8jdEQ==
|
integrity sha512-HlnDQD3r4YqCj2gu6uo86oEM0ixBsyKLaPcZcGwWAD5mFG5R4zzTZG7BO2wJkGWmkzijHluE14dlTmfzc8jdEQ==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.2.2.tgz#3bc75984e1d5ec8f59eb53702cc382d8e1be2061"
|
|
||||||
integrity sha512-d6jT8xgfKYFkzR7J0OHo2D+kFvY/6W8qEo6/hmdrTt6AKAqxs//rbbcdoyn3YQq1x6FVUUd39zzpezZntg9Naw==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz#e429f826279894be9096be6bec13e75e3d6bd671"
|
|
||||||
integrity sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==
|
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@12.2.0":
|
"@next/swc-linux-arm64-musl@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.0.tgz#c781ac642ad35e0578d8a8d19c638b0f31c1a334"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.0.tgz#c781ac642ad35e0578d8a8d19c638b0f31c1a334"
|
||||||
|
@ -2334,16 +2266,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.1.tgz#7ed5981b7afd3d9c4678ff36e1dd7f06a5f0c3d6"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.1.tgz#7ed5981b7afd3d9c4678ff36e1dd7f06a5f0c3d6"
|
||||||
integrity sha512-P8AkWd4RHbuF24ol3jk2akXpntcDI0gv5uD7eMpAOXb8W2A6y/sv0tKNSGUV3efSutOyu23jNn2EiTNxHgU4NQ==
|
integrity sha512-P8AkWd4RHbuF24ol3jk2akXpntcDI0gv5uD7eMpAOXb8W2A6y/sv0tKNSGUV3efSutOyu23jNn2EiTNxHgU4NQ==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.2.2.tgz#270db73e07a18d999f61e79a917943fa5bc1ef56"
|
|
||||||
integrity sha512-rIZRFxI9N/502auJT1i7coas0HTHUM+HaXMyJiCpnY8Rimbo0495ir24tzzHo3nQqJwcflcPTwEh/DV17sdv9A==
|
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz#1f276c0784a5ca599bfa34b2fcc0b38f3a738e08"
|
|
||||||
integrity sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==
|
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@12.2.0":
|
"@next/swc-linux-x64-gnu@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.0.tgz#0e2235a59429eadd40ac8880aec18acdbc172a31"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.0.tgz#0e2235a59429eadd40ac8880aec18acdbc172a31"
|
||||||
|
@ -2354,16 +2276,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.1.tgz#0bb3e5162b189cb4d88761ff1781896781c7bd65"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.1.tgz#0bb3e5162b189cb4d88761ff1781896781c7bd65"
|
||||||
integrity sha512-ZbsM+rIMqK6xi3lovspzPJoIPre3LglKrCXKLkln7rD0uiymzfLhS2VCj8u4qRynz22iAzuI4mJNpZa3AsJFrA==
|
integrity sha512-ZbsM+rIMqK6xi3lovspzPJoIPre3LglKrCXKLkln7rD0uiymzfLhS2VCj8u4qRynz22iAzuI4mJNpZa3AsJFrA==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.2.2.tgz#e6c72fa20478552e898c434f4d4c0c5e89d2ea78"
|
|
||||||
integrity sha512-ir1vNadlUDj7eQk15AvfhG5BjVizuCHks9uZwBfUgT5jyeDCeRvaDCo1+Q6+0CLOAnYDR/nqSCvBgzG2UdFh9A==
|
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz#1d9933dd6ba303dcfd8a2acd6ac7c27ed41e2eea"
|
|
||||||
integrity sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==
|
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@12.2.0":
|
"@next/swc-linux-x64-musl@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.0.tgz#b0a10db0d9e16f079429588a58f71fa3c3d46178"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.0.tgz#b0a10db0d9e16f079429588a58f71fa3c3d46178"
|
||||||
|
@ -2374,16 +2286,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.1.tgz#64e983e38a5e86bc613bfc46e0b92a1787ba5392"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.1.tgz#64e983e38a5e86bc613bfc46e0b92a1787ba5392"
|
||||||
integrity sha512-JeATguMe37bviPwkIUjO7T3kcefMBQwJFLhkFTaJYGmPm12EsW1FtKcg87AI87xdGvfrHQKlM3phNaG/dkneTQ==
|
integrity sha512-JeATguMe37bviPwkIUjO7T3kcefMBQwJFLhkFTaJYGmPm12EsW1FtKcg87AI87xdGvfrHQKlM3phNaG/dkneTQ==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.2.2.tgz#b9ef9efe2c401839cdefa5e70402386aafdce15a"
|
|
||||||
integrity sha512-bte5n2GzLN3O8JdSFYWZzMgEgDHZmRz5wiispiiDssj4ik3l8E7wq/czNi8RmIF+ioj2sYVokUNa/ekLzrESWw==
|
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz#2ef9837f12ca652b1783d72ecb86208906042f02"
|
|
||||||
integrity sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==
|
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@12.2.0":
|
"@next/swc-win32-arm64-msvc@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.0.tgz#3063f850c9db7b774c69e9be74ad59986cf6fc34"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.0.tgz#3063f850c9db7b774c69e9be74ad59986cf6fc34"
|
||||||
|
@ -2394,16 +2296,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.1.tgz#2394b05230f0011a01010524e25d8f4ec71e27e1"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.1.tgz#2394b05230f0011a01010524e25d8f4ec71e27e1"
|
||||||
integrity sha512-8dal/MdrVshDKYBtloJw/RhJx140KUoRRYoRfpJ9oAdP8UXBdR0haKfg5EdOy98t8Q76apArxPsK7DfwoR1f3w==
|
integrity sha512-8dal/MdrVshDKYBtloJw/RhJx140KUoRRYoRfpJ9oAdP8UXBdR0haKfg5EdOy98t8Q76apArxPsK7DfwoR1f3w==
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.2.2.tgz#18fa7ec7248da3a7926a0601d9ececc53ac83157"
|
|
||||||
integrity sha512-ZUGCmcDmdPVSAlwJ/aD+1F9lYW8vttseiv4n2+VCDv5JloxiX9aY32kYZaJJO7hmTLNrprvXkb4OvNuHdN22Jg==
|
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz#74003d0aa1c59dfa56cb15481a5c607cbc0027b9"
|
|
||||||
integrity sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==
|
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@12.2.0":
|
"@next/swc-win32-ia32-msvc@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.0.tgz#001bbadf3d2cf006c4991f728d1d23e4d5c0e7cc"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.0.tgz#001bbadf3d2cf006c4991f728d1d23e4d5c0e7cc"
|
||||||
|
@ -2414,16 +2306,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.1.tgz#90acd18e63e7620992ee3f7d3dec80ccc7120f9e"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.1.tgz#90acd18e63e7620992ee3f7d3dec80ccc7120f9e"
|
||||||
integrity sha512-uSAoOBpCp4oxVD9gTY1f27hr9xNLEOCglxZPH1+FonHpM5n9Sp4H01uQHWE/Y26iHmJeUJAWxtRxEYylnO4U9A==
|
integrity sha512-uSAoOBpCp4oxVD9gTY1f27hr9xNLEOCglxZPH1+FonHpM5n9Sp4H01uQHWE/Y26iHmJeUJAWxtRxEYylnO4U9A==
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.2.2.tgz#54936e84f4a219441d051940354da7cd3eafbb4f"
|
|
||||||
integrity sha512-v7ykeEDbr9eXiblGSZiEYYkWoig6sRhAbLKHUHQtk8vEWWVEqeXFcxmw6LRrKu5rCN1DY357UlYWToCGPQPCRA==
|
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@12.1.6":
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz#a350caf42975e7197b24b495b8d764eec7e6a36e"
|
|
||||||
integrity sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==
|
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@12.2.0":
|
"@next/swc-win32-x64-msvc@12.2.0":
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.0.tgz#9f66664f9122ca555b96a5f2fc6e2af677bf801b"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.0.tgz#9f66664f9122ca555b96a5f2fc6e2af677bf801b"
|
||||||
|
@ -2434,11 +2316,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.1.tgz#f3b186c8f7278656c7690a64f362d0d5b1d738af"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.1.tgz#f3b186c8f7278656c7690a64f362d0d5b1d738af"
|
||||||
integrity sha512-gx4aLMAZAVjtShiCrUSszoxnzBWJWf09Lkey6mcc0jFZjbz4xkyDbp53V229DtOYTUL4t0IZJ0I7+ftQ5CYIjg==
|
integrity sha512-gx4aLMAZAVjtShiCrUSszoxnzBWJWf09Lkey6mcc0jFZjbz4xkyDbp53V229DtOYTUL4t0IZJ0I7+ftQ5CYIjg==
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@12.2.2":
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.2.2.tgz#7460be700a60d75816f01109400b51fe929d7e89"
|
|
||||||
integrity sha512-2D2iinWUL6xx8D9LYVZ5qi7FP6uLAoWymt8m8aaG2Ld/Ka8/k723fJfiklfuAcwOxfufPJI+nRbT5VcgHGzHAQ==
|
|
||||||
|
|
||||||
"@node-redis/client@^1.0.1":
|
"@node-redis/client@^1.0.1":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.6.tgz#de8bfe6cfdc5781f0021ce9d18a11c821c948d9d"
|
resolved "https://registry.yarnpkg.com/@node-redis/client/-/client-1.0.6.tgz#de8bfe6cfdc5781f0021ce9d18a11c821c948d9d"
|
||||||
|
@ -3041,6 +2918,29 @@
|
||||||
"@radix-ui/react-use-previous" "0.1.1"
|
"@radix-ui/react-use-previous" "0.1.1"
|
||||||
"@radix-ui/react-use-size" "0.1.1"
|
"@radix-ui/react-use-size" "0.1.1"
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group@^0.1.5":
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-0.1.5.tgz#9e4d65e22c4fc0ba3a42fbc8d5496c430e5e9852"
|
||||||
|
integrity sha512-Yp14wFiqe00azF+sG5CCJz4JGOP/f5Jj+CxLlZCmMpG5qhVTWeaeG4YH6pvX4KL41fS8x9FAaLb8wW9y01o67g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.1.0"
|
||||||
|
"@radix-ui/react-context" "0.1.1"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
"@radix-ui/react-roving-focus" "0.1.5"
|
||||||
|
"@radix-ui/react-toggle" "0.1.4"
|
||||||
|
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle@0.1.4":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-0.1.4.tgz#c5c63f7cc5a03556bb58e0a763735b41bb0331f9"
|
||||||
|
integrity sha512-gxUq6NgMc4ChV8VJnwdYqueeoblspwXHAexYo+jM9N2hFLbI1C587jLjdTHzIcUa9q68Xaw4jtiImWDOokEhRw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.1.0"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-tooltip@^0.1.0":
|
"@radix-ui/react-tooltip@^0.1.0":
|
||||||
version "0.1.7"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz#6f8c00d6e489565d14abf209ce0fb8853c8c8ee3"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-0.1.7.tgz#6f8c00d6e489565d14abf209ce0fb8853c8c8ee3"
|
||||||
|
@ -3144,6 +3044,20 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@reach/observe-rect@^1.1.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|
||||||
|
"@reach/portal@^0.16.0":
|
||||||
|
version "0.16.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.16.2.tgz#ca83696215ee03acc2bb25a5ae5d8793eaaf2f64"
|
||||||
|
integrity sha512-9ur/yxNkuVYTIjAcfi46LdKUvH0uYZPfEp4usWcpt6PIp+WDF57F/5deMe/uGi/B/nfDweQu8VVwuMVrCb97JQ==
|
||||||
|
dependencies:
|
||||||
|
"@reach/utils" "0.16.0"
|
||||||
|
tiny-warning "^1.0.3"
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@reach/skip-nav@^0.11.2":
|
"@reach/skip-nav@^0.11.2":
|
||||||
version "0.11.2"
|
version "0.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/skip-nav/-/skip-nav-0.11.2.tgz#015498b2125ad8ef1e48cb8ab33dca93925fcbc8"
|
resolved "https://registry.yarnpkg.com/@reach/skip-nav/-/skip-nav-0.11.2.tgz#015498b2125ad8ef1e48cb8ab33dca93925fcbc8"
|
||||||
|
@ -3161,6 +3075,14 @@
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
warning "^4.0.3"
|
warning "^4.0.3"
|
||||||
|
|
||||||
|
"@reach/utils@0.16.0":
|
||||||
|
version "0.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce"
|
||||||
|
integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==
|
||||||
|
dependencies:
|
||||||
|
tiny-warning "^1.0.3"
|
||||||
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@rollup/pluginutils@^4.2.1":
|
"@rollup/pluginutils@^4.2.1":
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
||||||
|
@ -3858,6 +3780,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
|
||||||
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
|
integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==
|
||||||
|
|
||||||
|
"@types/json-logic-js@^1.2.1":
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/json-logic-js/-/json-logic-js-1.2.1.tgz#064e777b77b0fcb77f00c2c50fec1387cb33eb47"
|
||||||
|
integrity sha512-g/g+wj/7sgazpiCHiyAtndoNiy/LodLkNG4I9MILAl0UinKKwv3GiPKbtvcE1hIoezQqgDamXfx8Lht62/hHqw==
|
||||||
|
|
||||||
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9":
|
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.9":
|
||||||
version "7.0.11"
|
version "7.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
|
@ -4015,6 +3942,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-gtm-module@^2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-gtm-module/-/react-gtm-module-2.0.1.tgz#b2c6cd14ec251d6ae7fa576edf1d43825908a378"
|
||||||
|
integrity sha512-T/DN9gAbCYk5wJ1nxf4pSwmXz4d1iVjM++OoG+mwMfz9STMAotGjSb65gJHOS5bPvl6vLSsJnuC+y/43OQrltg==
|
||||||
|
|
||||||
"@types/react-phone-number-input@^3.0.13":
|
"@types/react-phone-number-input@^3.0.13":
|
||||||
version "3.0.13"
|
version "3.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-phone-number-input/-/react-phone-number-input-3.0.13.tgz#4eb7dcd278dcf9eb2a8d2ce2cb304657cbf1b4e5"
|
resolved "https://registry.yarnpkg.com/@types/react-phone-number-input/-/react-phone-number-input-3.0.13.tgz#4eb7dcd278dcf9eb2a8d2ce2cb304657cbf1b4e5"
|
||||||
|
@ -5942,6 +5874,11 @@ clone@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
|
||||||
|
|
||||||
|
clone@^2.1.2:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||||
|
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
|
||||||
|
|
||||||
clsx@^1.1.1:
|
clsx@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
|
@ -6042,6 +5979,11 @@ comma-separated-tokens@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
|
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
|
||||||
integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==
|
integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==
|
||||||
|
|
||||||
|
command-score@^0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/command-score/-/command-score-0.1.2.tgz#b986ad7e8c0beba17552a56636c44ae38363d381"
|
||||||
|
integrity sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==
|
||||||
|
|
||||||
commander@6.2.0:
|
commander@6.2.0:
|
||||||
version "6.2.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75"
|
||||||
|
@ -6807,6 +6749,11 @@ dotenv@^10.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||||
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||||
|
|
||||||
|
dotenv@^16.0.1:
|
||||||
|
version "16.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||||
|
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||||
|
|
||||||
dotenv@^8.2.0:
|
dotenv@^8.2.0:
|
||||||
version "8.6.0"
|
version "8.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
||||||
|
@ -8001,6 +7948,11 @@ fast-equals@^1.6.3:
|
||||||
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
|
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.3.tgz#84839a1ce20627c463e1892f2ae316380c81b459"
|
||||||
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
|
integrity sha512-4WKW0AL5+WEqO0zWavAfYGY1qwLsBgE//DN4TTcVEN2UlINgkv9b3vm2iHicoenWKSX9mKWmGOsU/iI5IST7pQ==
|
||||||
|
|
||||||
|
fast-equals@^2.0.3:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927"
|
||||||
|
integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==
|
||||||
|
|
||||||
fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
|
fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
|
||||||
version "3.2.11"
|
version "3.2.11"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
|
||||||
|
@ -9299,7 +9251,7 @@ immer@^9.0.12:
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc"
|
||||||
integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==
|
integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==
|
||||||
|
|
||||||
immutable@^3.x.x:
|
immutable@^3.8.2, immutable@^3.x.x:
|
||||||
version "3.8.2"
|
version "3.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||||
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
||||||
|
@ -10961,6 +10913,11 @@ json-buffer@3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||||
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||||
|
|
||||||
|
json-logic-js@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-logic-js/-/json-logic-js-2.0.2.tgz#b613e095f5e598cb78f7b9a2bbf638e74cf98158"
|
||||||
|
integrity sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==
|
||||||
|
|
||||||
json-parse-better-errors@^1.0.1:
|
json-parse-better-errors@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||||
|
@ -11126,6 +11083,17 @@ jws@^4.0.0:
|
||||||
jwa "^2.0.0"
|
jwa "^2.0.0"
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
kbar@^0.1.0-beta.36:
|
||||||
|
version "0.1.0-beta.36"
|
||||||
|
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.36.tgz#29f21337979b84ffdceb8a319d80fe89b5461936"
|
||||||
|
integrity sha512-i5tU7VYkMmxHCoyG5qzkNeU3qViKBz2F0fjqvWWSKsgVABCF3BjxzAH570Mhn3Zy92x3NGZae8emkBpEk7MKgw==
|
||||||
|
dependencies:
|
||||||
|
"@reach/portal" "^0.16.0"
|
||||||
|
command-score "^0.1.2"
|
||||||
|
fast-equals "^2.0.3"
|
||||||
|
react-virtual "^2.8.2"
|
||||||
|
tiny-invariant "^1.2.0"
|
||||||
|
|
||||||
keccak@^3.0.0:
|
keccak@^3.0.0:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0"
|
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.2.tgz#4c2c6e8c54e04f2670ee49fa734eb9da152206e0"
|
||||||
|
@ -12659,21 +12627,6 @@ next-api-middleware@^1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
|
||||||
next-auth@^4.3.3:
|
|
||||||
version "4.10.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.0.tgz#cffa850cb0f633e6340d34c567634df1d10feabe"
|
|
||||||
integrity sha512-4CKZbv9VeCaqfDAXyqFThZy05ApbLd0bhXEB+DCq9aD43h6Rkvz0QgM7QOCJXESy0QKJUXHzopkBq+iaGxdc0g==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.16.3"
|
|
||||||
"@panva/hkdf" "^1.0.1"
|
|
||||||
cookie "^0.4.1"
|
|
||||||
jose "^4.3.7"
|
|
||||||
oauth "^0.9.15"
|
|
||||||
openid-client "^5.1.0"
|
|
||||||
preact "^10.6.3"
|
|
||||||
preact-render-to-string "^5.1.19"
|
|
||||||
uuid "^8.3.2"
|
|
||||||
|
|
||||||
next-auth@^4.9.0:
|
next-auth@^4.9.0:
|
||||||
version "4.9.0"
|
version "4.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.9.0.tgz#0d8cabcb22a976744131a2e68d5f08756f322593"
|
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.9.0.tgz#0d8cabcb22a976744131a2e68d5f08756f322593"
|
||||||
|
@ -12689,6 +12642,13 @@ next-auth@^4.9.0:
|
||||||
preact-render-to-string "^5.1.19"
|
preact-render-to-string "^5.1.19"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
|
next-axiom@^0.10.0:
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-axiom/-/next-axiom-0.10.0.tgz#7cd2f52d9691cf9f7984ed325d58a6f93912eed3"
|
||||||
|
integrity sha512-QrOUqNmJ20StiR0b+/HMiW0o0w442DjfaOg4yH3hNJmAX0c9Afy6hiZ/j9D67XmqlpXeg83ESx89rt83u4/giA==
|
||||||
|
dependencies:
|
||||||
|
whatwg-fetch "^3.6.2"
|
||||||
|
|
||||||
next-collect@^0.2.0:
|
next-collect@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-collect/-/next-collect-0.2.0.tgz#62ec8f5c263cd8bd6e1da26b5d456e072c6f6e4d"
|
resolved "https://registry.yarnpkg.com/next-collect/-/next-collect-0.2.0.tgz#62ec8f5c263cd8bd6e1da26b5d456e072c6f6e4d"
|
||||||
|
@ -12769,29 +12729,6 @@ next-validations@^0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.2.0.tgz#ce3c4bc332b115beda633521fd81e587987864eb"
|
resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.2.0.tgz#ce3c4bc332b115beda633521fd81e587987864eb"
|
||||||
integrity sha512-QMF2hRNSSbjeBaCYqpt3mEM9CkXXzaMCWCvPyi5/vKTBjbgkiYtaQnUfjj5eH8dX+ZmRrBYGgN1EKqL7ZnI0wQ==
|
integrity sha512-QMF2hRNSSbjeBaCYqpt3mEM9CkXXzaMCWCvPyi5/vKTBjbgkiYtaQnUfjj5eH8dX+ZmRrBYGgN1EKqL7ZnI0wQ==
|
||||||
|
|
||||||
next@12.1.6:
|
|
||||||
version "12.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-12.1.6.tgz#eb205e64af1998651f96f9df44556d47d8bbc533"
|
|
||||||
integrity sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==
|
|
||||||
dependencies:
|
|
||||||
"@next/env" "12.1.6"
|
|
||||||
caniuse-lite "^1.0.30001332"
|
|
||||||
postcss "8.4.5"
|
|
||||||
styled-jsx "5.0.2"
|
|
||||||
optionalDependencies:
|
|
||||||
"@next/swc-android-arm-eabi" "12.1.6"
|
|
||||||
"@next/swc-android-arm64" "12.1.6"
|
|
||||||
"@next/swc-darwin-arm64" "12.1.6"
|
|
||||||
"@next/swc-darwin-x64" "12.1.6"
|
|
||||||
"@next/swc-linux-arm-gnueabihf" "12.1.6"
|
|
||||||
"@next/swc-linux-arm64-gnu" "12.1.6"
|
|
||||||
"@next/swc-linux-arm64-musl" "12.1.6"
|
|
||||||
"@next/swc-linux-x64-gnu" "12.1.6"
|
|
||||||
"@next/swc-linux-x64-musl" "12.1.6"
|
|
||||||
"@next/swc-win32-arm64-msvc" "12.1.6"
|
|
||||||
"@next/swc-win32-ia32-msvc" "12.1.6"
|
|
||||||
"@next/swc-win32-x64-msvc" "12.1.6"
|
|
||||||
|
|
||||||
next@12.2.0:
|
next@12.2.0:
|
||||||
version "12.2.0"
|
version "12.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-12.2.0.tgz#aef47cd96b602bc1307d1dcf9a1ee3e753845544"
|
resolved "https://registry.yarnpkg.com/next/-/next-12.2.0.tgz#aef47cd96b602bc1307d1dcf9a1ee3e753845544"
|
||||||
|
@ -12818,32 +12755,6 @@ next@12.2.0:
|
||||||
"@next/swc-win32-ia32-msvc" "12.2.0"
|
"@next/swc-win32-ia32-msvc" "12.2.0"
|
||||||
"@next/swc-win32-x64-msvc" "12.2.0"
|
"@next/swc-win32-x64-msvc" "12.2.0"
|
||||||
|
|
||||||
next@^12.1.6:
|
|
||||||
version "12.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-12.2.2.tgz#029bf5e4a18a891ca5d05b189b7cd983fd22c072"
|
|
||||||
integrity sha512-zAYFY45aBry/PlKONqtlloRFqU/We3zWYdn2NoGvDZkoYUYQSJC8WMcalS5C19MxbCZLUVCX7D7a6gTGgl2yLg==
|
|
||||||
dependencies:
|
|
||||||
"@next/env" "12.2.2"
|
|
||||||
"@swc/helpers" "0.4.2"
|
|
||||||
caniuse-lite "^1.0.30001332"
|
|
||||||
postcss "8.4.5"
|
|
||||||
styled-jsx "5.0.2"
|
|
||||||
use-sync-external-store "1.1.0"
|
|
||||||
optionalDependencies:
|
|
||||||
"@next/swc-android-arm-eabi" "12.2.2"
|
|
||||||
"@next/swc-android-arm64" "12.2.2"
|
|
||||||
"@next/swc-darwin-arm64" "12.2.2"
|
|
||||||
"@next/swc-darwin-x64" "12.2.2"
|
|
||||||
"@next/swc-freebsd-x64" "12.2.2"
|
|
||||||
"@next/swc-linux-arm-gnueabihf" "12.2.2"
|
|
||||||
"@next/swc-linux-arm64-gnu" "12.2.2"
|
|
||||||
"@next/swc-linux-arm64-musl" "12.2.2"
|
|
||||||
"@next/swc-linux-x64-gnu" "12.2.2"
|
|
||||||
"@next/swc-linux-x64-musl" "12.2.2"
|
|
||||||
"@next/swc-win32-arm64-msvc" "12.2.2"
|
|
||||||
"@next/swc-win32-ia32-msvc" "12.2.2"
|
|
||||||
"@next/swc-win32-x64-msvc" "12.2.2"
|
|
||||||
|
|
||||||
next@^12.2.0:
|
next@^12.2.0:
|
||||||
version "12.2.1"
|
version "12.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-12.2.1.tgz#b487dc598ef1373a1b1275d68531a7088fe5653d"
|
resolved "https://registry.yarnpkg.com/next/-/next-12.2.1.tgz#b487dc598ef1373a1b1275d68531a7088fe5653d"
|
||||||
|
@ -13857,6 +13768,18 @@ playwright-core@1.22.1:
|
||||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.1.tgz#59ddf903546171fdfd9c3dc189630c883619667c"
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.1.tgz#59ddf903546171fdfd9c3dc189630c883619667c"
|
||||||
integrity sha512-H+ZUVYnceWNXrRf3oxTEKAr81QzFsCKu5Fp//fEjQvqgKkfA1iX3E9DBrPJpPNOrgVzcE+IqeI0fDmYJe6Ynnw==
|
integrity sha512-H+ZUVYnceWNXrRf3oxTEKAr81QzFsCKu5Fp//fEjQvqgKkfA1iX3E9DBrPJpPNOrgVzcE+IqeI0fDmYJe6Ynnw==
|
||||||
|
|
||||||
|
playwright-core@1.22.2:
|
||||||
|
version "1.22.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a"
|
||||||
|
integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q==
|
||||||
|
|
||||||
|
playwright@^1.22.2:
|
||||||
|
version "1.22.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.22.2.tgz#353a7c29f89ca9600edc7a9a30aed790823c797d"
|
||||||
|
integrity sha512-hUTpg7LytIl3/O4t0AQJS1V6hWsaSY5uZ7w1oCC8r3a1AQN5d6otIdCkiB3cbzgQkcMaRxisinjMFMVqZkybdQ==
|
||||||
|
dependencies:
|
||||||
|
playwright-core "1.22.2"
|
||||||
|
|
||||||
pngjs@^3.0.0, pngjs@^3.3.3:
|
pngjs@^3.0.0, pngjs@^3.3.3:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||||
|
@ -14356,6 +14279,23 @@ raw-body@2.5.1:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
react-awesome-query-builder@^5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-awesome-query-builder/-/react-awesome-query-builder-5.1.2.tgz#2a8e34e4558275471069ca5b39d73113e748cf84"
|
||||||
|
integrity sha512-qh+vcu0Cgo1OaGS6uNiXNSNd2ORUGAtXUhwqqoGuI1LqXwXBQgKVBKg3/uaVL7T7BK8LYvJQiKu94S9+C9Fh3Q==
|
||||||
|
dependencies:
|
||||||
|
"@date-io/moment" "^1.3.13"
|
||||||
|
classnames "^2.3.1"
|
||||||
|
clone "^2.1.2"
|
||||||
|
immutable "^3.8.2"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
moment "^2.29.1"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-redux "^7.2.2"
|
||||||
|
redux "^4.1.0"
|
||||||
|
spel2js "^0.2.8"
|
||||||
|
sqlstring "^2.3.2"
|
||||||
|
|
||||||
react-calendar@^3.3.1:
|
react-calendar@^3.3.1:
|
||||||
version "3.7.0"
|
version "3.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-3.7.0.tgz#951d56e91afb33b1c1e019cb790349fbffcc6894"
|
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-3.7.0.tgz#951d56e91afb33b1c1e019cb790349fbffcc6894"
|
||||||
|
@ -14453,6 +14393,11 @@ react-fit@^1.4.0:
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
|
react-gtm-module@^2.0.11:
|
||||||
|
version "2.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-gtm-module/-/react-gtm-module-2.0.11.tgz#14484dac8257acd93614e347c32da9c5ac524206"
|
||||||
|
integrity sha512-8gyj4TTxeP7eEyc2QKawEuQoAZdjKvMY4pgWfycGmqGByhs17fR+zEBs0JUDq4US/l+vbTl+6zvUIx27iDo/Vw==
|
||||||
|
|
||||||
react-hook-form@^7.16.2:
|
react-hook-form@^7.16.2:
|
||||||
version "7.33.1"
|
version "7.33.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0"
|
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0"
|
||||||
|
@ -14582,7 +14527,7 @@ react-reconciler@^0.26.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
scheduler "^0.20.2"
|
||||||
|
|
||||||
react-redux@^7.2.4:
|
react-redux@^7.2.2, react-redux@^7.2.4:
|
||||||
version "7.2.8"
|
version "7.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
|
||||||
integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
|
integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
|
||||||
|
@ -14702,6 +14647,13 @@ react-use-intercom@1.5.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-use-intercom/-/react-use-intercom-1.5.1.tgz#94567a80ce3b56692962d712a54489c55fb4c54e"
|
resolved "https://registry.yarnpkg.com/react-use-intercom/-/react-use-intercom-1.5.1.tgz#94567a80ce3b56692962d712a54489c55fb4c54e"
|
||||||
integrity sha512-rsSiW3j6yv0bBWCaX+VOCK/ndh/VzntlYjxHLHhV++iQtFurFhcRD249rF07ZaLH8ZP5SR6FzP3Alqdi3usBQg==
|
integrity sha512-rsSiW3j6yv0bBWCaX+VOCK/ndh/VzntlYjxHLHhV++iQtFurFhcRD249rF07ZaLH8ZP5SR6FzP3Alqdi3usBQg==
|
||||||
|
|
||||||
|
react-virtual@^2.8.2:
|
||||||
|
version "2.10.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.10.4.tgz#08712f0acd79d7d6f7c4726f05651a13b24d8704"
|
||||||
|
integrity sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==
|
||||||
|
dependencies:
|
||||||
|
"@reach/observe-rect" "^1.1.0"
|
||||||
|
|
||||||
react-virtualized-auto-sizer@^1.0.6:
|
react-virtualized-auto-sizer@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca"
|
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca"
|
||||||
|
@ -14836,6 +14788,13 @@ redux@^4.0.0, redux@^4.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.2"
|
"@babel/runtime" "^7.9.2"
|
||||||
|
|
||||||
|
redux@^4.1.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
||||||
|
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.9.2"
|
||||||
|
|
||||||
reflect-metadata@0.1.13, reflect-metadata@^0.1.13:
|
reflect-metadata@0.1.13, reflect-metadata@^0.1.13:
|
||||||
version "0.1.13"
|
version "0.1.13"
|
||||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||||
|
@ -15754,6 +15713,11 @@ spdx-license-ids@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95"
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95"
|
||||||
integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==
|
integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==
|
||||||
|
|
||||||
|
spel2js@^0.2.8:
|
||||||
|
version "0.2.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/spel2js/-/spel2js-0.2.8.tgz#3ba3b291e5c6bae5c9f703e839294969b61fc691"
|
||||||
|
integrity sha512-dzYq+v4YV7SPIdNrmvFAUjc0HcgI7b0yoMw7kzOBmlj/GjdOb/+8dVn1I7nLuOS5X2SW+LK3tf2SVkXRjCkWBA==
|
||||||
|
|
||||||
split-string@^3.0.1, split-string@^3.0.2:
|
split-string@^3.0.1, split-string@^3.0.2:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||||
|
@ -16440,7 +16404,12 @@ timm@^1.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f"
|
resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f"
|
||||||
integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==
|
integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==
|
||||||
|
|
||||||
tiny-warning@^1.0.0:
|
tiny-invariant@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
|
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
|
||||||
|
|
||||||
|
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
@ -17832,7 +17801,7 @@ whatwg-encoding@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
|
|
||||||
whatwg-fetch@^3.4.1:
|
whatwg-fetch@^3.4.1, whatwg-fetch@^3.6.2:
|
||||||
version "3.6.2"
|
version "3.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
|
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
|
||||||
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
|
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
|
||||||
|
|
Loading…
Reference in New Issue
Block a user