From 171827f5479a71ef7ca941d39a7bb20d618d8735 Mon Sep 17 00:00:00 2001 From: Rob Jackson Date: Wed, 28 Jun 2023 17:22:51 +0100 Subject: [PATCH] chore: recategorize apps (#9306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove unused code in InstalledAppsLayout * Add new app categories "crm", "conferencing" and "messaging" * Sort getAppCategories entries alphabetically * Fix 404s on new category pages (and remove hardcoded category lists) * Fix admin apps list not showing "no available apps" for new categories * Recategorise apps * Sync seed-app-store categories with config files * Replace unnecessary seed-app-store.config.json with appStoreMetadata * Copy video.svg to conferencing.svg * Add messaging.svg * Remove web3 from getAppCategories (used by installed apps, admin apps) * Fix app-store-cli categories - Add conferencing - Add CRM - Remove video - Remove web3 * Remove outdated web3 comment in seed-app-store * Update apps/web/public/static/locales/en/common.json * Add cron script to keep db apps in sync with app metadata * Add redirect for app category "video" to "conferencing" * Fix up "video" category overrides to apply to conferencing * Fix conferencing apps not showing as a location for non-team users * Restore "installed_app" string for conferencing apps * Make linter happier * Remove my "installed_app_conferencing_description" as this was fixed upstream * Quick tidy up * Add dry-run to syncAppMeta via CRON_ENABLE_APP_SYNC env * Replace console.log with logger in syncAppMeta --------- Co-authored-by: Peer Richelsen Co-authored-by: alannnc Co-authored-by: Hariom Balhara Co-authored-by: Omar López --- .env.example | 4 + .github/workflows/cron-syncAppMeta.yml | 24 ++ .github/workflows/env-create-file.yml | 1 + app.json | 4 + .../components/eventtype/EventSetupTab.tsx | 12 +- apps/web/next.config.js | 10 + apps/web/pages/api/cron/syncAppMeta.ts | 67 +++++ apps/web/pages/apps/installed/[category].tsx | 36 +-- .../public/app-categories/conferencing.svg | 1 + apps/web/public/app-categories/messaging.svg | 1 + apps/web/public/static/locales/en/common.json | 10 +- .../src/components/AppCreateUpdateForm.tsx | 15 +- packages/app-store/_utils/getAppCategories.ts | 58 +++-- .../app-store/_utils/getInstalledAppPath.ts | 4 +- packages/app-store/around/config.json | 2 +- packages/app-store/campfire/config.json | 2 +- packages/app-store/closecom/config.json | 2 +- packages/app-store/dailyvideo/_metadata.ts | 4 +- packages/app-store/discord/config.json | 2 +- packages/app-store/eightxeight/config.json | 2 +- packages/app-store/facetime/config.json | 2 +- packages/app-store/googlevideo/_metadata.ts | 4 +- packages/app-store/hubspot/_metadata.ts | 2 +- packages/app-store/huddle01video/_metadata.ts | 4 +- packages/app-store/jitsivideo/_metadata.ts | 2 +- packages/app-store/mirotalk/config.json | 2 +- packages/app-store/office365video/config.json | 3 +- packages/app-store/ping/config.json | 2 +- packages/app-store/raycast/config.json | 2 +- packages/app-store/riverside/config.json | 2 +- packages/app-store/routing-forms/config.json | 2 +- packages/app-store/salesforce/config.json | 2 +- packages/app-store/sendgrid/config.json | 2 +- packages/app-store/signal/config.json | 2 +- packages/app-store/sirius_video/config.json | 2 +- packages/app-store/sylapsvideo/config.json | 2 +- packages/app-store/tandemvideo/_metadata.ts | 4 +- packages/app-store/telegram/config.json | 2 +- .../config.json | 2 +- packages/app-store/typeform/config.json | 2 +- packages/app-store/utils.ts | 20 +- packages/app-store/vital/_metadata.ts | 4 +- packages/app-store/webex/config.json | 2 +- packages/app-store/whatsapp/config.json | 2 +- packages/app-store/whereby/config.json | 2 +- .../app-store/wipemycalother/_metadata.ts | 4 +- packages/app-store/zohocrm/config.json | 2 +- packages/app-store/zoomvideo/_metadata.ts | 4 +- packages/features/apps/AdminAppsList.tsx | 2 +- .../migration.sql | 10 + packages/prisma/schema.prisma | 6 +- packages/prisma/seed-app-store.config.json | 244 ------------------ packages/prisma/seed-app-store.ts | 53 ++-- .../routers/viewer/apps/listLocal.handler.ts | 2 +- .../routers/viewer/apps/toggle.handler.ts | 10 +- packages/types/environment.d.ts | 1 + turbo.json | 1 + 57 files changed, 304 insertions(+), 373 deletions(-) create mode 100644 .github/workflows/cron-syncAppMeta.yml create mode 100644 apps/web/pages/api/cron/syncAppMeta.ts create mode 100755 apps/web/public/app-categories/conferencing.svg create mode 100644 apps/web/public/app-categories/messaging.svg create mode 100644 packages/prisma/migrations/20230603115613_reorganise_app_categories/migration.sql delete mode 100644 packages/prisma/seed-app-store.config.json diff --git a/.env.example b/.env.example index b82c907d70..d6cf1908bc 100644 --- a/.env.example +++ b/.env.example @@ -81,6 +81,10 @@ CALCOM_TELEMETRY_DISABLED= # ApiKey for cronjobs CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0' +# Whether to automatically keep app metadata in the database in sync with the metadata/config files. When disabled, the +# sync runs in a reporting-only dry-run mode. +CRON_ENABLE_APP_SYNC=false + # Application Key for symmetric encryption and decryption # must be 32 bytes for AES256 encryption algorithm # You can use: `openssl rand -base64 24` to generate one diff --git a/.github/workflows/cron-syncAppMeta.yml b/.github/workflows/cron-syncAppMeta.yml new file mode 100644 index 0000000000..baea2304e3 --- /dev/null +++ b/.github/workflows/cron-syncAppMeta.yml @@ -0,0 +1,24 @@ +name: Cron - syncAppMeta + +on: + workflow_dispatch: + # "Scheduled workflows run on the latest commit on the default or base branch." + # — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule + schedule: + # Runs “Every month at 1st (see https://crontab.guru) + - cron: "0 0 1 * *" +jobs: + cron-syncAppMeta: + env: + APP_URL: ${{ secrets.APP_URL }} + CRON_API_KEY: ${{ secrets.CRON_API_KEY }} + runs-on: ubuntu-latest + steps: + - name: cURL request + if: ${{ env.APP_URL && env.CRON_API_KEY }} + run: | + curl ${{ secrets.APP_URL }}/api/cron/syncAppMeta \ + -X POST \ + -H 'content-type: application/json' \ + -H 'authorization: ${{ secrets.CRON_API_KEY }}' \ + --fail diff --git a/.github/workflows/env-create-file.yml b/.github/workflows/env-create-file.yml index 8089d9949f..6cf7f6dcdb 100644 --- a/.github/workflows/env-create-file.yml +++ b/.github/workflows/env-create-file.yml @@ -11,6 +11,7 @@ env: INPUT_ENV_GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }} INPUT_ENV_GOOGLE_LOGIN_ENABLED: true # INPUT_ENV_CRON_API_KEY: xxx + # INPUT_ENV_CRON_ENABLE_APP_SYNC: true|false INPUT_ENV_CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} INPUT_ENV_NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }} INPUT_ENV_STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} diff --git a/app.json b/app.json index 5180797014..4078d8be61 100644 --- a/app.json +++ b/app.json @@ -37,6 +37,10 @@ "description": "ApiKey for cronjobs", "value": "" }, + "CRON_ENABLE_APP_SYNC": { + "description": "Whether to automatically keep app metadata in the database in sync with the metadata/config files. When disabled, the sync runs in a reporting-only dry-run mode.", + "value": "false" + }, "SEND_FEEDBACK_EMAIL": { "description": "Send feedback email", "value": "" diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index d06adcb2dc..17382ba553 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -117,8 +117,16 @@ export const EventSetupTab = ( const [selectedLocation, setSelectedLocation] = useState(undefined); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration); - const locationOptions = props.locationOptions.filter((option) => { - return !team ? option.label !== "Conferencing" : true; + const locationOptions = props.locationOptions.map((locationOption) => { + const options = locationOption.options.filter((option) => { + // Skip "Organizer's Default App" for non-team members + return !team ? option.label !== t("organizer_default_conferencing_app") : true; + }); + + return { + ...locationOption, + options, + }; }); const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({ diff --git a/apps/web/next.config.js b/apps/web/next.config.js index d9b5f80a90..73c0d38219 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -493,6 +493,16 @@ const nextConfig = { destination: "/event-types?openIntercom=true", permanent: true, }, + { + source: "/apps/categories/video", + destination: "/apps/categories/conferencing", + permanent: true, + }, + { + source: "/apps/installed/video", + destination: "/apps/installed/conferencing", + permanent: true, + }, ]; if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") { diff --git a/apps/web/pages/api/cron/syncAppMeta.ts b/apps/web/pages/api/cron/syncAppMeta.ts new file mode 100644 index 0000000000..fbb9383bc3 --- /dev/null +++ b/apps/web/pages/api/cron/syncAppMeta.ts @@ -0,0 +1,67 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; +import logger from "@calcom/lib/logger"; +import { prisma } from "@calcom/prisma"; +import type { AppCategories, Prisma } from "@calcom/prisma/client"; + +const isDryRun = process.env.CRON_ENABLE_APP_SYNC !== "true"; +const log = logger.getChildLogger({ + prefix: ["[api/cron/syncAppMeta]", ...(isDryRun ? ["(dry-run)"] : [])], +}); + +/** + * syncAppMeta makes sure any app metadata that has been replicated into the database + * remains synchronized with any changes made to the app config files. + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const apiKey = req.headers.authorization || req.query.apiKey; + if (process.env.CRON_API_KEY !== apiKey) { + res.status(401).json({ message: "Not authenticated" }); + return; + } + if (req.method !== "POST") { + res.status(405).json({ message: "Invalid method" }); + return; + } + + log.info(`🧐 Checking DB apps are in-sync with app metadata`); + + const dbApps = await prisma.app.findMany(); + + for await (const dbApp of dbApps) { + const app = await getAppWithMetadata(dbApp); + const updates: Prisma.AppUpdateManyMutationInput = {}; + + if (!app) { + log.warn(`💀 App ${dbApp.slug} (${dbApp.dirName}) no longer exists.`); + continue; + } + + // Check for any changes in the app categories (tolerates changes in ordering) + if ( + dbApp.categories.length !== app.categories.length || + !dbApp.categories.every((category) => app.categories.includes(category)) + ) { + updates["categories"] = app.categories as AppCategories[]; + } + + if (dbApp.dirName !== (app.dirName ?? app.slug)) { + updates["dirName"] = app.dirName ?? app.slug; + } + + if (Object.keys(updates).length > 0) { + log.info(`🔨 Updating app ${dbApp.slug} with ${Object.keys(updates).join(", ")}`); + if (!isDryRun) { + await prisma.app.update({ + where: { slug: dbApp.slug }, + data: updates, + }); + } + } else { + log.info(`✅ App ${dbApp.slug} is up-to-date and correct`); + } + } + + res.json({ ok: true }); +} diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index f0261754db..03caec4c47 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -6,11 +6,11 @@ import { AppSettings } from "@calcom/app-store/_components/AppSettings"; import { InstallAppButton } from "@calcom/app-store/components"; import type { EventLocationType } from "@calcom/app-store/locations"; import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; -import { InstalledAppVariants } from "@calcom/app-store/utils"; import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog"; import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { AppCategories } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import type { App } from "@calcom/types/App"; @@ -29,11 +29,14 @@ import { DropdownItem, showToast, } from "@calcom/ui"; +import type { LucideIcon } from "@calcom/ui/components/icon"; import { BarChart, Calendar, + Contact, CreditCard, Grid, + Mail, MoreHorizontal, Plus, Share2, @@ -101,8 +104,8 @@ function ConnectOrDisconnectIntegrationMenuItem(props: { } interface IntegrationsContainerProps { - variant?: (typeof InstalledAppVariants)[number]; - exclude?: (typeof InstalledAppVariants)[number][]; + variant?: AppCategories; + exclude?: AppCategories[]; handleDisconnect: (credentialId: number) => void; } @@ -225,14 +228,19 @@ const IntegrationsContainer = ({ }: IntegrationsContainerProps): JSX.Element => { const { t } = useLocale(); const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true }); - const emptyIcon = { + + // TODO: Refactor and reuse getAppCategories? + const emptyIcon: Record = { calendar: Calendar, conferencing: Video, automation: Share2, analytics: BarChart, payment: CreditCard, - web3: BarChart, + web3: BarChart, // deprecated other: Grid, + video: Video, // deprecated + messaging: Mail, + crm: Contact, }; return ( @@ -267,9 +275,7 @@ const IntegrationsContainer = ({ className="mb-6" actions={