From 58d1c28e9df8527335e476d6b770afaaf2f22056 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 14 Jul 2022 18:10:53 +0530 Subject: [PATCH] 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 Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com> --- .github/workflows/e2e-app-store.yml | 99 ++++ .vscode/tasks.json | 4 - apps/web/components/App.tsx | 422 +++++++------- apps/web/components/NavTabs.tsx | 6 +- apps/web/components/PencilEdit.tsx | 56 ++ apps/web/components/Shell.tsx | 154 +++--- apps/web/components/apps/AllApps.tsx | 1 + apps/web/components/apps/AppCard.tsx | 15 +- .../components/apps/TrendingAppsSlider.tsx | 1 + apps/web/components/ui/form/Select.tsx | 71 ++- apps/web/next.config.js | 5 + apps/web/package.json | 1 + apps/web/pages/api/app-store/[...static].ts | 19 +- apps/web/pages/api/integrations/[...args].ts | 53 +- apps/web/pages/apps/[slug]/[...pages].tsx | 124 +++++ apps/web/pages/apps/[slug]/index.tsx | 2 + apps/web/pages/apps/categories/[category].tsx | 7 +- apps/web/public/static/locales/en/common.json | 1 + apps/web/server/routers/viewer.tsx | 26 +- apps/web/tsconfig.json | 2 + package.json | 6 +- packages/app-store-cli/package.json | 3 +- packages/app-store-cli/src/app-store.ts | 89 ++- packages/app-store/_appRegistry.ts | 11 +- .../_apps-playwright/config/globalSetup.ts | 19 + .../config/playwright.config.ts | 57 ++ .../_apps-playwright/lib/testUtils.ts | 1 + packages/app-store/_baseApp/_metadata.ts | 2 - .../app-store/_utils/useAddAppMutation.ts | 16 +- packages/app-store/apps.browser.generated.tsx | 2 + packages/app-store/apps.server.generated.ts | 2 + packages/app-store/components.tsx | 80 ++- .../app-store/ee/routing_forms/_metadata.ts | 18 + .../app-store/ee/routing_forms/api/add.ts | 25 + .../app-store/ee/routing_forms/api/index.ts | 2 + .../routing_forms/api/responses/[formId].ts | 68 +++ .../components/RoutingNavBar.tsx | 28 + .../routing_forms/components/RoutingShell.tsx | 33 ++ .../ee/routing_forms/components/SideBar.tsx | 105 ++++ .../ee/routing_forms/components/index.ts | 2 + .../config/config.tsx | 133 +++++ .../react-awesome-query-builder/styles.css | 125 +++++ .../react-awesome-query-builder/widgets.tsx | 300 ++++++++++ .../app-store/ee/routing_forms/config.json | 16 + packages/app-store/ee/routing_forms/env.d.ts | 1 + packages/app-store/ee/routing_forms/index.ts | 3 + .../app-store/ee/routing_forms/package.json | 23 + .../pages/app-routing.config.tsx | 14 + .../pages/form-edit/[...appPages].tsx | 402 ++++++++++++++ .../pages/forms/[...appPages].tsx | 267 +++++++++ .../pages/route-builder/[...appPages].tsx | 518 ++++++++++++++++++ .../pages/routing-link/[...appPages].tsx | 266 +++++++++ .../playwright/config/globalSetup.ts | 45 ++ .../playwright/config/globalTeardown.ts | 18 + .../playwright/config/playwright.config.ts | 12 + .../playwright/fixtures/fixtures.ts | 5 + .../routing_forms/playwright/lib/testUtils.ts | 12 + .../playwright/tests/basic.test.ts | 111 ++++ .../ee/routing_forms/static/icon.svg | 3 + .../app-store/ee/routing_forms/trpc-router.ts | 182 ++++++ packages/app-store/ee/routing_forms/utils.ts | 30 + packages/app-store/ee/routing_forms/zod.ts | 39 ++ packages/app-store/next.d.ts | 2 + packages/app-store/trpc-routers.ts | 4 + packages/app-store/tsconfig.json | 20 +- packages/app-store/types.d.ts | 8 +- packages/lib/notification.ts | 1 + .../migration.sql | 33 ++ packages/prisma/schema.prisma | 76 ++- packages/prisma/seed-app-store.config.json | 6 + packages/prisma/seed-app-store.ts | 1 + packages/types/App.d.ts | 2 + packages/types/AppGetServerSideProps.d.ts | 16 + packages/types/AppHandler.d.ts | 15 + packages/ui/BooleanToggleGroup.tsx | 56 ++ packages/ui/EmptyScreen.tsx | 4 +- packages/ui/index.tsx | 1 + tests/config/globalSetup.ts | 2 +- turbo.json | 5 + yarn.lock | 375 ++++++------- 80 files changed, 4223 insertions(+), 567 deletions(-) create mode 100644 .github/workflows/e2e-app-store.yml create mode 100644 apps/web/components/PencilEdit.tsx create mode 100644 apps/web/pages/apps/[slug]/[...pages].tsx create mode 100644 packages/app-store/_apps-playwright/config/globalSetup.ts create mode 100644 packages/app-store/_apps-playwright/config/playwright.config.ts create mode 100644 packages/app-store/_apps-playwright/lib/testUtils.ts create mode 100644 packages/app-store/ee/routing_forms/_metadata.ts create mode 100644 packages/app-store/ee/routing_forms/api/add.ts create mode 100644 packages/app-store/ee/routing_forms/api/index.ts create mode 100644 packages/app-store/ee/routing_forms/api/responses/[formId].ts create mode 100644 packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx create mode 100644 packages/app-store/ee/routing_forms/components/RoutingShell.tsx create mode 100644 packages/app-store/ee/routing_forms/components/SideBar.tsx create mode 100644 packages/app-store/ee/routing_forms/components/index.ts create mode 100644 packages/app-store/ee/routing_forms/components/react-awesome-query-builder/config/config.tsx create mode 100644 packages/app-store/ee/routing_forms/components/react-awesome-query-builder/styles.css create mode 100644 packages/app-store/ee/routing_forms/components/react-awesome-query-builder/widgets.tsx create mode 100644 packages/app-store/ee/routing_forms/config.json create mode 100644 packages/app-store/ee/routing_forms/env.d.ts create mode 100644 packages/app-store/ee/routing_forms/index.ts create mode 100644 packages/app-store/ee/routing_forms/package.json create mode 100644 packages/app-store/ee/routing_forms/pages/app-routing.config.tsx create mode 100644 packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx create mode 100644 packages/app-store/ee/routing_forms/pages/forms/[...appPages].tsx create mode 100644 packages/app-store/ee/routing_forms/pages/route-builder/[...appPages].tsx create mode 100644 packages/app-store/ee/routing_forms/pages/routing-link/[...appPages].tsx create mode 100644 packages/app-store/ee/routing_forms/playwright/config/globalSetup.ts create mode 100644 packages/app-store/ee/routing_forms/playwright/config/globalTeardown.ts create mode 100644 packages/app-store/ee/routing_forms/playwright/config/playwright.config.ts create mode 100644 packages/app-store/ee/routing_forms/playwright/fixtures/fixtures.ts create mode 100644 packages/app-store/ee/routing_forms/playwright/lib/testUtils.ts create mode 100644 packages/app-store/ee/routing_forms/playwright/tests/basic.test.ts create mode 100644 packages/app-store/ee/routing_forms/static/icon.svg create mode 100644 packages/app-store/ee/routing_forms/trpc-router.ts create mode 100644 packages/app-store/ee/routing_forms/utils.ts create mode 100644 packages/app-store/ee/routing_forms/zod.ts create mode 100644 packages/app-store/trpc-routers.ts create mode 100644 packages/prisma/migrations/20220616072241_app_routing_forms/migration.sql create mode 100644 packages/types/AppGetServerSideProps.d.ts create mode 100644 packages/types/AppHandler.d.ts create mode 100644 packages/ui/BooleanToggleGroup.tsx diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml new file mode 100644 index 0000000000..deb42ffe07 --- /dev/null +++ b/.github/workflows/e2e-app-store.yml @@ -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 \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1f3424f531..6ca5d40f3c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -22,10 +22,6 @@ "group": { "kind": "build", "isDefault": true - }, - // Try start the task on folder open - "runOptions": { - "runOn": "folderOpen" } }, { diff --git a/apps/web/components/App.tsx b/apps/web/components/App.tsx index 9abb8aab85..0ce1fbbe09 100644 --- a/apps/web/components/App.tsx +++ b/apps/web/components/App.tsx @@ -12,15 +12,20 @@ import { ChevronLeftIcon } from "@heroicons/react/solid"; import Link from "next/link"; import React, { useEffect, useState } from "react"; +import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; import { InstallAppButton } from "@calcom/app-store/components"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import showToast from "@calcom/lib/notification"; import { App as AppType } from "@calcom/types/App"; import { Button, SkeletonButton } from "@calcom/ui"; +import LicenseRequired from "@ee/components/LicenseRequired"; + +import { trpc } from "@lib/trpc"; import Shell from "@components/Shell"; import Badge from "@components/ui/Badge"; -export default function App({ +const Component = ({ name, type, logo, @@ -36,25 +41,19 @@ export default function App({ email, tos, privacy, -}: { - 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; -}) { + isProOnly, +}: Parameters[0]) => { 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", { style: "currency", @@ -90,176 +89,233 @@ export default function App({ getInstalledApp(type); }, [type]); const allowedMultipleInstalls = categories.indexOf("calendar") > -1; - return ( - <> - -
-
- - - {t("browse_apps")} - - -
-
- {name} -
-

{name}

-

- {categories[0]} • {t("published_by", { author })} -

-
-
-
- {!isLoading ? ( - isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? ( -
- - ( - - )} - /> -
- ) : ( - ( - - )} - /> - ) - ) : ( - - )} - {price !== 0 && ( - - {feeType === "usage-based" - ? commission + "% + " + priceInDollar + "/booking" - : priceInDollar} - {feeType === "monthly" && "/" + t("month")} - - )} + return ( +
+
+ + + {t("browse_apps")} + + +
+
+ {name} +
+
+

{name}

+ {isProOnly && user?.plan === "FREE" ? ( + + PRO + + ) : null}
-
- {/* reintroduce once we show permissions and features - */} +

+ {categories[0]} • {t("published_by", { author })} +

+
-
-
{body}
-
-

{t("categories")}

-
- {categories.map((category) => ( - - - {category} - - - ))} -
-

{t("pricing")}

- - {price === 0 ? ( - "Free" - ) : ( - <> - {Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - useGrouping: false, - }).format(price)} - {feeType === "monthly" && "/" + t("month")} - - )} +
+ {!isLoading ? ( + isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? ( +
+ + { + if (useDefaultComponent) { + props = { + onClick: () => { + mutation.mutate({ type }); + }, + loading: mutation.isLoading, + }; + } + return ( + + ); + }} + /> +
+ ) : ( + { + if (useDefaultComponent) { + props = { + onClick: () => { + mutation.mutate({ type }); + }, + loading: mutation.isLoading, + }; + } + return ( + + ); + }} + /> + ) + ) : ( + + )} + {price !== 0 && ( + + {feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar} + {feeType === "monthly" && "/" + t("month")} -

{t("learn_more")}

- -
- - Every app published on the Cal.com App Store is open source and thoroughly tested via peer - reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are - published by Cal.com. If you encounter inappropriate content or behaviour please report it. - - - Report App - -
+ )}
- - + {/* reintroduce once we show permissions and features + */} +
+ +
+
{body}
+
+

{t("categories")}

+
+ {categories.map((category) => ( + + + {category} + + + ))} +
+

{t("pricing")}

+ + {price === 0 ? ( + "Free" + ) : ( + <> + {Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + useGrouping: false, + }).format(price)} + {feeType === "monthly" && "/" + t("month")} + + )} + +

{t("learn_more")}

+ +
+ + Every app published on the Cal.com App Store is open source and thoroughly tested via peer + reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are + published by Cal.com. If you encounter inappropriate content or behaviour please report it. + + + Report App + +
+
+
+ ); +}; + +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 ( + + {props.licenseRequired ? ( + + + + ) : ( + + )} + ); } diff --git a/apps/web/components/NavTabs.tsx b/apps/web/components/NavTabs.tsx index 361caf96d2..0a113d63fd 100644 --- a/apps/web/components/NavTabs.tsx +++ b/apps/web/components/NavTabs.tsx @@ -18,6 +18,7 @@ export interface NavTabProps { tabName?: string; icon?: SVGComponent; adminRequired?: boolean; + className?: string; }[]; linkProps?: Omit; } @@ -58,7 +59,7 @@ const NavTabs: FC = ({ tabs, linkProps, ...props }) => { : noop; const Component = tab.adminRequired ? AdminRequired : Fragment; - + const className = tab.className || ""; return ( @@ -68,7 +69,8 @@ const NavTabs: FC = ({ tabs, linkProps, ...props }) => { isCurrent ? "border-neutral-900 text-neutral-900" : "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}> {tab.icon && ( diff --git a/apps/web/components/PencilEdit.tsx b/apps/web/components/PencilEdit.tsx new file mode 100644 index 0000000000..0d958e3480 --- /dev/null +++ b/apps/web/components/PencilEdit.tsx @@ -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 ( +
+ {editIcon ? ( + <> +

+ {value} +

+ {!readOnly ? ( + + ) : null} + + ) : ( +
+ { + setEditIcon(true); + onChange(e.target.value); + }} + /> +
+ )} +
+ ); +} diff --git a/apps/web/components/Shell.tsx b/apps/web/components/Shell.tsx index b3d94447b7..ae7f296144 100644 --- a/apps/web/components/Shell.tsx +++ b/apps/web/components/Shell.tsx @@ -1,4 +1,5 @@ import { SelectorIcon } from "@heroicons/react/outline"; +import { CollectionIcon } from "@heroicons/react/solid"; import { ArrowLeftIcon, CalendarIcon, @@ -125,6 +126,12 @@ const Layout = ({ }: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => { const isEmbed = useIsEmbed(); const router = useRouter(); + const { data: routingForms } = trpc.useQuery([ + "viewer.appById", + { + appId: "routing_forms", + }, + ]); const { t } = useLocale(); const navigation = [ @@ -146,6 +153,14 @@ const Layout = ({ icon: ClockIcon, 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"), href: "/workflows", @@ -157,7 +172,7 @@ const Layout = ({ name: t("apps"), href: "/apps", icon: ViewGridIcon, - current: router.asPath.startsWith("/apps"), + current: router.asPath.startsWith("/apps") && !router.asPath.startsWith("/apps/routing_forms/"), child: [ { name: t("app_store"), @@ -212,7 +227,6 @@ const Layout = ({
- {/* logo icon for tablet */} @@ -220,53 +234,55 @@ const Layout = ({ )} {/* add padding to content for mobile navigation*/} @@ -453,7 +469,7 @@ export default function Shell(props: LayoutProps) { const i18n = useViewerI18n(); 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. // 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 [helpOpen, setHelpOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); - + if (!user) { + return null; + } const onHelpItemSelect = () => { setHelpOpen(false); setMenuOpen(false); @@ -514,14 +532,14 @@ function UserDropdown({ small }: { small?: boolean }) { // eslint-disable-next-line @next/next/no-img-element {user?.username } - {!user?.away && ( + {!user.away && (
)} - {user?.away && ( + {user.away && (
)} @@ -529,10 +547,10 @@ function UserDropdown({ small }: { small?: boolean }) { - {user?.name || "Nameless User"} + {user.name || "Nameless User"} - {user?.username + {user.username ? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? `cal.com/${user.username}` : `/${user.username}` @@ -555,24 +573,24 @@ function UserDropdown({ small }: { small?: boolean }) { { - mutation.mutate({ away: !user?.away }); + mutation.mutate({ away: user?.away }); utils.invalidateQueries("viewer.me"); }} className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"> - {user?.username && ( + {user.username && ( ))}
diff --git a/apps/web/components/apps/AppCard.tsx b/apps/web/components/apps/AppCard.tsx index ae77a1e1d5..c6a6ee17f4 100644 --- a/apps/web/components/apps/AppCard.tsx +++ b/apps/web/components/apps/AppCard.tsx @@ -2,6 +2,10 @@ import Link from "next/link"; import Button from "@calcom/ui/Button"; +import { trpc } from "@lib/trpc"; + +import Badge from "@components/ui/Badge"; + interface AppCardProps { logo: string; name: string; @@ -10,9 +14,11 @@ interface AppCardProps { description: string; rating: number; reviews?: number; + isProOnly?: boolean; } export default function AppCard(props: AppCardProps) { + const { data: user } = trpc.useQuery(["viewer.me"]); return (
-

{props.name}

+
+

{props.name}

+ {props.isProOnly && user?.plan === "FREE" ? ( + + PRO + + ) : null} +
{/* TODO: add reviews
{props.rating} stars {props.reviews} reviews diff --git a/apps/web/components/apps/TrendingAppsSlider.tsx b/apps/web/components/apps/TrendingAppsSlider.tsx index 1d938e9520..943e603628 100644 --- a/apps/web/components/apps/TrendingAppsSlider.tsx +++ b/apps/web/components/apps/TrendingAppsSlider.tsx @@ -30,6 +30,7 @@ const TrendingAppsSlider = ({ items }: { items: T[] }) => { logo={app.logo} rating={app.rating} reviews={app.reviews} + isProOnly={app.isProOnly} /> )} /> diff --git a/apps/web/components/ui/form/Select.tsx b/apps/web/components/ui/form/Select.tsx index f3e32443eb..3615501798 100644 --- a/apps/web/components/ui/form/Select.tsx +++ b/apps/web/components/ui/form/Select.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import ReactSelect, { components, GroupBase, Props, InputProps } from "react-select"; +import React, { useCallback, useEffect, useState } from "react"; +import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select"; 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
+
+ { + mutation.mutate({ ...form, disabled: !isChecked }); + }} + label={!form.disabled ? t("Disable Form") : t("Enable Form")} + /> +
+
+ + + + + + {t("Download responses (CSV)")} + + + + + {t("delete")} + + { + 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. + + +
+
+ ); +} diff --git a/packages/app-store/ee/routing_forms/components/index.ts b/packages/app-store/ee/routing_forms/components/index.ts new file mode 100644 index 0000000000..e191e97eec --- /dev/null +++ b/packages/app-store/ee/routing_forms/components/index.ts @@ -0,0 +1,2 @@ +export { default as InstallAppButton } from "./InstallAppButton"; +export { default as Icon } from "./icon"; diff --git a/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/config/config.tsx b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/config/config.tsx new file mode 100644 index 0000000000..792b1ec50c --- /dev/null +++ b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/config/config.tsx @@ -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 (props: T1 | undefined, Component: React.FC) { + if (!props) { + return
; + } + return ; +}; + +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
; + } + return ; + }, + valuePlaceholder: "Enter Phone Number", + }, + email: { + ...BasicConfig.widgets.text, + factory: (props) => { + if (!props) { + return
; + } + return ; + }, + }, +}; + +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; diff --git a/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/styles.css b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/styles.css new file mode 100644 index 0000000000..888375275f --- /dev/null +++ b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/styles.css @@ -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; +} diff --git a/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/widgets.tsx b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/widgets.tsx new file mode 100644 index 0000000000..aeba9e5450 --- /dev/null +++ b/packages/app-store/ee/routing_forms/components/react-awesome-query-builder/widgets.tsx @@ -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) => { + const val = e.target.value; + setValue(val); + }; + + const textValue = value || ""; + return ( +