chore: recategorize apps (#9306)

* 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 <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Rob Jackson 2023-06-28 17:22:51 +01:00 committed by GitHub
parent 8cfb9902ff
commit 171827f547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 304 additions and 373 deletions

View File

@ -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

24
.github/workflows/cron-syncAppMeta.yml vendored Normal file
View File

@ -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

View File

@ -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 }}

View File

@ -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": ""

View File

@ -117,8 +117,16 @@ export const EventSetupTab = (
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(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) => ({

View File

@ -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") {

View File

@ -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 });
}

View File

@ -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<AppCategories, LucideIcon> = {
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={
<Button
href={
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
}
href={variant ? `/apps/categories/${variant}` : "/apps"}
color="secondary"
StartIcon={Plus}>
{t("add")}
@ -285,7 +291,7 @@ const IntegrationsContainer = ({
};
const querySchema = z.object({
category: z.enum(InstalledAppVariants),
category: z.nativeEnum(AppCategories),
});
type querySchemaType = z.infer<typeof querySchema>;
@ -299,13 +305,11 @@ export default function InstalledApps() {
const { t } = useLocale();
const router = useRouter();
const category = router.query.category as querySchemaType["category"];
const categoryList: querySchemaType["category"][] = [
"payment",
"conferencing",
"automation",
"analytics",
"web3",
];
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
// Exclude calendar and other from categoryList, we handle those slightly differently below
return !(category in { other: null, calendar: null });
});
const [data, updateData] = useReducer(
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -863,12 +863,14 @@
"free_to_use_apps": "Free",
"no_category_apps": "No {{category}} apps",
"no_category_apps_description_calendar": "Add a calendar app to check for conflicts to prevent double bookings",
"no_category_apps_description_conferencing": "Try adding a conference app to intergrate video call with your clients",
"no_category_apps_description_conferencing": "Try adding a conference app for video calls with your clients",
"no_category_apps_description_payment": "Add a payment app to ease transaction between you and your clients",
"no_category_apps_description_analytics": "Add an analytics app for your booking pages",
"no_category_apps_description_automation": "Add an automation app to use",
"no_category_apps_description_other": "Add any other type of app to do all sorts of things",
"no_category_apps_description_web3": "Add a web3 app for your booking pages",
"no_category_apps_description_messaging": "Add a messaging app to set up custom notifications & reminders",
"no_category_apps_description_crm": "Add a CRM app to keep track of who you've met with",
"installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.",
"installed_app_payment_description": "Configure which payment processing services to use when charging your clients.",
"installed_app_analytics_description": "Configure which analytics apps to use for your booking pages",
@ -876,6 +878,8 @@
"installed_app_conferencing_description": "Configure which conferencing apps to use",
"installed_app_automation_description": "Configure which automation apps to use",
"installed_app_web3_description": "Configure which web3 apps to use for your booking pages",
"installed_app_messaging_description": "Configure which messaging apps to use for setting up custom notifications & reminders",
"installed_app_crm_description": "Configure which CRM apps to use for keeping track of who you've met with",
"analytics": "Analytics",
"empty_installed_apps_headline": "No apps installed",
"empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.",
@ -1286,6 +1290,8 @@
"connect_analytics_apps": "Connect analytics apps",
"connect_other_apps": "Connect other apps",
"connect_web3_apps": "Connect web3 apps",
"connect_messaging_apps": "Connect messaging apps",
"connect_crm_apps": "Connect CRM apps",
"current_step_of_total": "Step {{currentStep}} of {{maxSteps}}",
"add_variable": "Add variable",
"custom_phone_number": "Custom phone number",
@ -1895,6 +1901,8 @@
"set_up_your_profile_description": "Let people know who you are within {{orgName}}, and when they engage with your public link.",
"my_profile": "My Profile",
"my_settings": "My Settings",
"crm": "CRM",
"messaging": "Messaging",
"sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)",
"google_new_spam_policy": "Googles new spam policy could prevent you from receiving any email and calendar notifications about this booking.",
"resolve": "Resolve",

View File

@ -82,14 +82,17 @@ export const AppForm = ({
label: "Category of App",
name: "category",
type: "select",
// TODO: Refactor and reuse getAppCategories or type as Record<AppCategories,> to enforce consistency
options: [
{ label: "Calendar", value: "calendar" },
{ label: "Video", value: "video" },
{ label: "Payment", value: "payment" },
{ label: "Messaging", value: "messaging" },
{ label: "Web3", value: "web3" },
{ label: "Automation", value: "automation" },
// Manually sorted alphabetically
{ label: "Analytics", value: "analytics" },
{ label: "Automation", value: "automation" },
{ label: "Calendar", value: "calendar" },
{ label: "Conferencing", value: "conferencing" },
{ label: "CRM", value: "crm" },
{ label: "Messaging", value: "messaging" },
{ label: "Payment", value: "payment" },
{ label: "Other", value: "other" },
],
defaultValue: "",

View File

@ -1,5 +1,16 @@
import { WEBAPP_URL } from "@calcom/lib/constants";
import { Calendar, Video, CreditCard, Share2, BarChart, Grid } from "@calcom/ui/components/icon";
import type { AppCategories } from "@calcom/prisma/enums";
import type { LucideIcon } from "@calcom/ui/components/icon";
import {
Calendar,
Video,
CreditCard,
Share2,
BarChart,
Grid,
Mail,
Contact,
} from "@calcom/ui/components/icon";
function getHref(baseURL: string, category: string, useQueryParam: boolean) {
const baseUrlParsed = new URL(baseURL, WEBAPP_URL);
@ -7,8 +18,26 @@ function getHref(baseURL: string, category: string, useQueryParam: boolean) {
return useQueryParam ? `${baseUrlParsed.toString()}` : `${baseURL}/${category}`;
}
const getAppCategories = (baseURL: string, useQueryParam: boolean) => {
type AppCategoryEntry = {
name: AppCategories;
href: string;
icon: LucideIcon;
};
const getAppCategories = (baseURL: string, useQueryParam: boolean): AppCategoryEntry[] => {
// Manually sorted alphabetically, but leaving "Other" at the end
// TODO: Refactor and type with Record<AppCategories, AppCategoryEntry> to enforce consistency
return [
{
name: "analytics",
href: getHref(baseURL, "analytics", useQueryParam),
icon: BarChart,
},
{
name: "automation",
href: getHref(baseURL, "automation", useQueryParam),
icon: Share2,
},
{
name: "calendar",
href: getHref(baseURL, "calendar", useQueryParam),
@ -19,26 +48,21 @@ const getAppCategories = (baseURL: string, useQueryParam: boolean) => {
href: getHref(baseURL, "conferencing", useQueryParam),
icon: Video,
},
{
name: "crm",
href: getHref(baseURL, "crm", useQueryParam),
icon: Contact,
},
{
name: "messaging",
href: getHref(baseURL, "messaging", useQueryParam),
icon: Mail,
},
{
name: "payment",
href: getHref(baseURL, "payment", useQueryParam),
icon: CreditCard,
},
{
name: "automation",
href: getHref(baseURL, "automation", useQueryParam),
icon: Share2,
},
{
name: "analytics",
href: getHref(baseURL, "analytics", useQueryParam),
icon: BarChart,
},
{
name: "web3",
href: getHref(baseURL, "web3", useQueryParam),
icon: BarChart,
},
{
name: "other",
href: getHref(baseURL, "other", useQueryParam),

View File

@ -1,8 +1,8 @@
import z from "zod";
import { InstalledAppVariants } from "../utils";
import { AppCategories } from "@calcom/prisma/enums";
const variantSchema = z.enum(InstalledAppVariants);
const variantSchema = z.nativeEnum(AppCategories);
export default function getInstalledAppPath(
{ variant, slug }: { variant?: string; slug?: string },

View File

@ -7,7 +7,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com",
"email": "help@cal.com",
"description": "Discover radically unique video calls designed to help hybrid-remote teams create, collaborate and celebrate together.",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Feel connected with your remote team. Team events, new hire onboardings, coffee chats, all on Campfire. No more awkward Zoom calls.\r\r",

View File

@ -7,7 +7,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "other",
"categories": ["other"],
"categories": ["crm"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",

View File

@ -9,10 +9,10 @@ export const metadata = {
type: "daily_video",
variant: "conferencing",
url: "https://daily.co",
categories: ["video"],
categories: ["conferencing"],
logo: "icon.svg",
publisher: "Cal.com",
category: "video",
category: "conferencing",
slug: "daily-video",
title: "Cal Video",
isGlobal: true,

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://discord.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"appData": {

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://github.com/shivamklr",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Shivam Kalra",
"email": "shivamkalra98@gmail.com",
"appData": {

View File

@ -7,7 +7,7 @@
"logo": "icon.svg",
"url": "https://github.com/Mythie",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Lucas Smith",
"email": "help@cal.com",
"description": "Facetime makes it super simple for collaborating teams to jump on a video call.",

View File

@ -8,8 +8,8 @@ export const metadata = {
description: _package.description,
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
slug: "google-meet",
category: "video",
categories: ["video"],
category: "conferencing",
categories: ["conferencing"],
type: "google_video",
title: "Google Meet",
variant: "conferencing",

View File

@ -11,7 +11,7 @@ export const metadata = {
logo: "icon.svg",
publisher: "Cal.com",
url: "https://hubspot.com/",
categories: ["other"],
categories: ["crm"],
label: "HubSpot CRM",
slug: "hubspot",
title: "HubSpot CRM",

View File

@ -9,11 +9,11 @@ export const metadata = {
installed: true,
type: "huddle01_video",
variant: "conferencing",
categories: ["video", "web3"],
categories: ["conferencing"],
logo: "icon.svg",
publisher: "huddle01.com",
url: "https://huddle01.com",
category: "web3",
category: "conferencing",
slug: "huddle01",
title: "Huddle01",
isGlobal: false,

View File

@ -8,7 +8,7 @@ export const metadata = {
installed: true,
type: "jitsi_video",
variant: "conferencing",
categories: ["video"],
categories: ["conferencing"],
logo: "icon.svg",
publisher: "Cal.com",
url: "https://jitsi.org/",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"appData": {

View File

@ -9,8 +9,7 @@
"verified": true,
"rating": 4.3,
"reviews": 69,
"category": "video",
"categories": ["video"],
"categories": ["conferencing"],
"slug": "msteams",
"title": "MS Teams (Requires work/school account)",
"trending": true,

View File

@ -7,7 +7,7 @@
"logo": "icon.svg",
"url": "https://ping.gg",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Ping.gg",
"email": "support@ping.gg",
"description": "Ping.gg makes high quality video collaborations easier than ever. Think 'Zoom for streamers and creators'. Join a call in 3 clicks, manage audio and video like a pro, and copy-paste your guests straight into OBS",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://github.com/eluce2",
"variant": "other",
"categories": ["other"],
"categories": ["automation"],
"publisher": "Eric Luce",
"email": "info@restlessmindstech.com",
"description": "Quickly share your Cal.com meeting links with Raycast",

View File

@ -6,7 +6,7 @@
"logo": "icon-dark.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Your online recording studio. The easiest way to record podcasts and videos in studio quality from anywhere. All from the browser.",

View File

@ -7,7 +7,7 @@
"logo": "icon-dark.svg",
"url": "https://cal.com/resources/feature/routing-forms",
"variant": "other",
"categories": ["other"],
"categories": ["automation"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"licenseRequired": true,

View File

@ -6,7 +6,7 @@
"logo": "icon.png",
"url": "https://cal.com/",
"variant": "other_calendar",
"categories": ["other"],
"categories": ["crm"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.",

View File

@ -6,7 +6,7 @@
"logo": "logo.png",
"url": "https://cal.com/",
"variant": "other_calendar",
"categories": ["other"],
"categories": ["crm"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["messaging"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "Schedule a chat with your guests or have a Signal Video call.",

View File

@ -6,7 +6,7 @@
"logo": "icon-dark.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "Video meetings made for music.\rCreate your own virtual music classroom, easily.",

View File

@ -7,7 +7,7 @@
"logo": "icon.svg",
"url": "https://sylaps.com",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Sylaps Inc",
"email": "support@sylaps.com",
"description": "Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.",

View File

@ -8,9 +8,9 @@ export const metadata = {
type: "tandem_video",
title: "Tandem Video",
variant: "conferencing",
categories: ["video"],
categories: ["conferencing"],
slug: "tandem",
category: "video",
category: "conferencing",
logo: "icon.svg",
publisher: "",
url: "",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["messaging"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "Schedule a chat with your guests or have a Telegram Video call.",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://example.com/link",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com Inc",
"email": "support@cal.com",
"appData": {

View File

@ -6,7 +6,7 @@
"logo": "icon-dark.svg",
"url": "https://cal.com/",
"variant": "other",
"categories": ["other"],
"categories": ["automation"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Adds a link to copy Typeform Redirect URL",

View File

@ -6,6 +6,7 @@ import type { TFunction } from "next-i18next";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import type { EventLocationType } from "@calcom/app-store/locations";
import { defaultLocations } from "@calcom/app-store/locations";
import { AppCategories } from "@calcom/prisma/enums";
import type { App, AppMeta } from "@calcom/types/App";
export * from "./_utils/getEventTypeAppData";
@ -37,16 +38,6 @@ const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
export type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
export const InstalledAppVariants = [
"conferencing",
"calendar",
"payment",
"analytics",
"automation",
"other",
"web3",
] as const;
export const ALL_APPS = Object.values(ALL_APPS_MAP);
export function getLocationGroupedOptions(integrations: ReturnType<typeof getApps>, t: TFunction) {
@ -58,8 +49,13 @@ export function getLocationGroupedOptions(integrations: ReturnType<typeof getApp
if (app.locationOption) {
// All apps that are labeled as a locationOption are video apps. Extract the secondary category if available
let category =
app.categories.length >= 2 ? app.categories.find((category) => category !== "video") : app.category;
if (!category) category = "video";
app.categories.length >= 2
? app.categories.find(
(category) =>
!([AppCategories.video, AppCategories.conferencing] as string[]).includes(category)
)
: app.category;
if (!category) category = AppCategories.conferencing;
const option = { ...app.locationOption, icon: app.logo, slug: app.slug };
if (apps[category]) {
apps[category] = [...apps[category], option];

View File

@ -6,8 +6,8 @@ export const metadata = {
name: "Vital",
description: _package.description,
installed: true,
category: "other",
categories: ["other"],
category: "automation",
categories: ["automation"],
logo: "icon.svg",
label: "Vital",
publisher: "Vital",

View File

@ -8,7 +8,7 @@
"logo": "/icon.ico",
"url": "https://github.com/aar2dee2",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "aar2dee2",
"email": "support@cal.com",
"description": "Create meetings with Cisco Webex",

View File

@ -6,7 +6,7 @@
"logo": "icon.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["messaging"],
"publisher": "Cal.com, Inc.",
"email": "support@cal.com",
"description": "Schedule a chat with your guests or have a WhatsApp Video call.",

View File

@ -7,7 +7,7 @@
"logo": "icon-dark.svg",
"url": "https://cal.com/",
"variant": "conferencing",
"categories": ["video"],
"categories": ["conferencing"],
"publisher": "Cal.com, Inc.",
"email": "help@cal.com",
"description": "Whereby makes it super simple for collaborating teams to jump on a video call.",

View File

@ -6,8 +6,8 @@ export const metadata = {
name: _package.name,
description: _package.description,
installed: true,
category: "other",
categories: ["other"],
category: "automation",
categories: ["automation"],
// If using static next public folder, can then be referenced from the base URL (/).
logo: "icon-dark.svg",
publisher: "Cal.com",

View File

@ -6,7 +6,7 @@
"logo": "icon.png",
"url": "https://github.com/jatinsandilya",
"variant": "other",
"categories": ["other"],
"categories": ["crm"],
"publisher": "Jatin Sandilya",
"email": "help@cal.com",
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day",

View File

@ -7,12 +7,12 @@ export const metadata = {
name: "Zoom Video",
description: _package.description,
type: "zoom_video",
categories: ["video"],
categories: ["conferencing"],
variant: "conferencing",
logo: "icon.svg",
publisher: "Cal.com",
url: "https://zoom.us/",
category: "video",
category: "conferencing",
slug: "zoom",
title: "Zoom Video",
email: "help@cal.com",

View File

@ -292,7 +292,7 @@ const AdminAppsListContainer = () => {
if (isLoading) return <SkeletonLoader />;
if (!apps) {
if (!apps || apps.length === 0) {
return (
<EmptyScreen
Icon={AlertCircle}

View File

@ -0,0 +1,10 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "AppCategories" ADD VALUE 'conferencing';
ALTER TYPE "AppCategories" ADD VALUE 'crm';

View File

@ -614,10 +614,12 @@ enum AppCategories {
messaging
other
payment
video
web3
video // deprecated, please use 'conferencing' instead
web3 // deprecated, we should no longer have any web3 apps
automation
analytics
conferencing
crm
}
model App {

View File

@ -1,244 +0,0 @@
[
{
"/*": "This file is deprecated now. No new entry should be added to it.",
"dirName": "routing-forms",
"categories": ["other"],
"slug": "routing-forms",
"type": "routing-forms_other"
},
{
"dirName": "whereby",
"categories": ["video"],
"slug": "whereby",
"type": "whereby_video"
},
{
"dirName": "around",
"categories": ["video"],
"slug": "around",
"type": "around_video"
},
{
"dirName": "riverside",
"categories": ["video"],
"slug": "riverside",
"type": "riverside_video"
},
{
"dirName": "typeform",
"categories": ["other"],
"slug": "typeform",
"type": "typeform_other"
},
{
"dirName": "ping",
"categories": ["video"],
"slug": "ping",
"type": "ping_video"
},
{
"dirName": "campfire",
"categories": ["video"],
"slug": "campfire",
"type": "campfire_video"
},
{
"dirName": "raycast",
"categories": ["other"],
"slug": "raycast",
"type": "raycast_other"
},
{
"dirName": "n8n",
"categories": ["automation"],
"slug": "n8n",
"type": "n8n_automation"
},
{
"dirName": "exchangecalendar",
"categories": ["calendar"],
"slug": "exchange",
"type": "exchange_calendar"
},
{
"dirName": "qr_code",
"categories": ["other"],
"slug": "qr_code",
"type": "qr_code_other"
},
{
"dirName": "weather_in_your_calendar",
"categories": ["other"],
"slug": "weather_in_your_calendar",
"type": "weather_in_your_calendar_other"
},
{
"dirName": "fathom",
"categories": ["analytics"],
"slug": "fathom",
"type": "fathom_analytics"
},
{
"dirName": "plausible",
"categories": ["analytics"],
"slug": "plausible",
"type": "plausible_analytics"
},
{
"dirName": "wordpress",
"categories": ["other"],
"slug": "wordpress",
"type": "wordpress_other"
},
{
"dirName": "ga4",
"categories": ["analytics"],
"slug": "ga4",
"type": "ga4_analytics"
},
{
"dirName": "pipedream",
"categories": ["automation"],
"slug": "pipedream",
"type": "pipedream_automation"
},
{
"dirName": "sirius_video",
"categories": ["video"],
"slug": "sirius_video",
"type": "sirius_video_video"
},
{
"dirName": "sendgrid",
"categories": ["other"],
"slug": "sendgrid",
"type": "sendgrid_other_calendar"
},
{
"dirName": "closecom",
"categories": ["other"],
"slug": "closecom",
"type": "closecom_other_calendar"
},
{
"dirName": "whatsapp",
"categories": ["video"],
"slug": "whatsapp",
"type": "whatsapp_video"
},
{
"dirName": "telegram",
"categories": ["video"],
"slug": "telegram",
"type": "telegram_video"
},
{
"dirName": "signal",
"categories": ["video"],
"slug": "signal",
"type": "signal_video"
},
{
"dirName": "vimcal",
"categories": ["calendar"],
"slug": "vimcal",
"type": "vimcal_other"
},
{
"dirName": "amie",
"categories": ["calendar"],
"slug": "amie",
"type": "amie_other"
},
{
"dirName": "booking-pages-tag",
"categories": ["analytics"],
"slug": "booking-pages-tag",
"type": "booking-pages-tag_other",
"isTemplate": true
},
{
"dirName": "event-type-app-card",
"categories": ["other"],
"slug": "event-type-app-card",
"type": "event-type-app-card_other",
"isTemplate": true
},
{
"dirName": "event-type-location-video-static",
"categories": ["video"],
"slug": "event-type-location-video-static",
"type": "event-type-location-video-static_other",
"isTemplate": true
},
{
"dirName": "general-app-settings",
"categories": ["other"],
"slug": "general-app-settings",
"type": "general-app-settings_other",
"isTemplate": true
},
{
"dirName": "link-as-an-app",
"categories": ["other"],
"slug": "link-as-an-app",
"type": "link-as-an-app_other",
"isTemplate": true
},
{
"dirName": "basic",
"categories": ["other"],
"slug": "basic",
"type": "basic_other",
"isTemplate": true
},
{
"dirName": "facetime",
"categories": ["video"],
"slug": "facetime",
"type": "facetime_video",
"isTemplate": false
},
{
"dirName": "zohocrm",
"categories": ["other"],
"slug": "zohocrm",
"type": "zohocrm_other_calendar",
"isTemplate": false
},
{
"dirName": "webex",
"categories": ["video"],
"slug": "webex",
"type": "webex_video",
"isTemplate": false
},
{
"dirName": "cron",
"categories": ["calendar"],
"slug": "cron",
"type": "cron_other",
"isTemplate": false
},
{
"dirName": "gtm",
"categories": ["analytics"],
"slug": "gtm",
"type": "gtm_analytics",
"isTemplate": false
},
{
"dirName": "discord",
"categories": ["video"],
"slug": "discord",
"type": "discord_video",
"isTemplate": false
},
{
"dirName": "sylapsvideo",
"categories": ["video"],
"slug": "sylapsvideo",
"type": "sylaps_video",
"isTemplate": false
}
]

View File

@ -4,10 +4,11 @@
*/
import type { Prisma } from "@prisma/client";
import dotEnv from "dotenv";
import fs from "fs";
import path from "path";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import prisma from ".";
import { AppCategories } from "./enums";
dotEnv.config({ path: "../../.env.appStore" });
@ -235,7 +236,7 @@ export default async function main() {
client_secret,
redirect_uris,
});
await createApp("google-meet", "googlevideo", ["video"], "google_video", {
await createApp("google-meet", "googlevideo", ["conferencing"], "google_video", {
client_id,
client_secret,
redirect_uris,
@ -248,7 +249,7 @@ export default async function main() {
client_id: process.env.MS_GRAPH_CLIENT_ID,
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
});
await createApp("msteams", "office365video", ["video"], "office365_video", {
await createApp("msteams", "office365video", ["conferencing"], "office365_video", {
client_id: process.env.MS_GRAPH_CLIENT_ID,
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
});
@ -266,45 +267,45 @@ export default async function main() {
}
// Video apps
if (process.env.DAILY_API_KEY) {
await createApp("daily-video", "dailyvideo", ["video"], "daily_video", {
await createApp("daily-video", "dailyvideo", ["conferencing"], "daily_video", {
api_key: process.env.DAILY_API_KEY,
scale_plan: process.env.DAILY_SCALE_PLAN,
});
}
if (process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET) {
await createApp("tandem", "tandemvideo", ["video"], "tandem_video", {
await createApp("tandem", "tandemvideo", ["conferencing"], "tandem_video", {
client_id: process.env.TANDEM_CLIENT_ID as string,
client_secret: process.env.TANDEM_CLIENT_SECRET as string,
base_url: (process.env.TANDEM_BASE_URL as string) || "https://tandem.chat",
});
}
if (process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET) {
await createApp("zoom", "zoomvideo", ["video"], "zoom_video", {
await createApp("zoom", "zoomvideo", ["conferencing"], "zoom_video", {
client_id: process.env.ZOOM_CLIENT_ID,
client_secret: process.env.ZOOM_CLIENT_SECRET,
});
}
await createApp("jitsi", "jitsivideo", ["video"], "jitsi_video");
await createApp("jitsi", "jitsivideo", ["conferencing"], "jitsi_video");
// Other apps
if (process.env.HUBSPOT_CLIENT_ID && process.env.HUBSPOT_CLIENT_SECRET) {
await createApp("hubspot", "hubspot", ["other"], "hubspot_other_calendar", {
await createApp("hubspot", "hubspot", ["crm"], "hubspot_other_calendar", {
client_id: process.env.HUBSPOT_CLIENT_ID,
client_secret: process.env.HUBSPOT_CLIENT_SECRET,
});
}
if (process.env.SALESFORCE_CONSUMER_KEY && process.env.SALESFORCE_CONSUMER_SECRET) {
await createApp("salesforce", "salesforce", ["other"], "salesforce_other_calendar", {
await createApp("salesforce", "salesforce", ["crm"], "salesforce_other_calendar", {
consumer_key: process.env.SALESFORCE_CONSUMER_KEY,
consumer_secret: process.env.SALESFORCE_CONSUMER_SECRET,
});
}
if (process.env.ZOHOCRM_CLIENT_ID && process.env.ZOHOCRM_CLIENT_SECRET) {
await createApp("zohocrm", "zohocrm", ["other"], "zohocrm_other_calendar", {
await createApp("zohocrm", "zohocrm", ["crm"], "zohocrm_other_calendar", {
client_id: process.env.ZOHOCRM_CLIENT_ID,
client_secret: process.env.ZOHOCRM_CLIENT_SECRET,
});
}
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
await createApp("wipe-my-cal", "wipemycalother", ["automation"], "wipemycal_other");
if (process.env.GIPHY_API_KEY) {
await createApp("giphy", "giphy", ["other"], "giphy_other", {
api_key: process.env.GIPHY_API_KEY,
@ -312,7 +313,7 @@ export default async function main() {
}
if (process.env.VITAL_API_KEY && process.env.VITAL_WEBHOOK_SECRET) {
await createApp("vital-automation", "vital", ["other"], "vital_other", {
await createApp("vital-automation", "vital", ["automation"], "vital_other", {
mode: process.env.VITAL_DEVELOPMENT_MODE || "sandbox",
region: process.env.VITAL_REGION || "us",
api_key: process.env.VITAL_API_KEY,
@ -326,8 +327,7 @@ export default async function main() {
});
}
// Web3 apps
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
await createApp("huddle01", "huddle01video", ["conferencing"], "huddle01_video");
// Payment apps
if (
@ -348,21 +348,22 @@ export default async function main() {
});
}
const generatedApps = JSON.parse(
fs.readFileSync(path.join(__dirname, "seed-app-store.config.json"), "utf8")
);
for (let i = 0; i < generatedApps.length; i++) {
const generatedApp = generatedApps[i];
if (generatedApp.isTemplate && process.argv[2] !== "seed-templates") {
for (const [, app] of Object.entries(appStoreMetadata)) {
if (app.isTemplate && process.argv[2] !== "seed-templates") {
continue;
}
const validatedCategories = app.categories.filter(
(category): category is AppCategories => category in AppCategories
);
await createApp(
generatedApp.slug,
generatedApp.dirName,
generatedApp.categories,
generatedApp.type,
app.slug,
app.dirName ?? app.slug,
validatedCategories,
app.type,
undefined,
generatedApp.isTemplate
app.isTemplate
);
}

View File

@ -17,7 +17,7 @@ type ListLocalOptions = {
export const listLocalHandler = async ({ ctx, input }: ListLocalOptions) => {
const { prisma } = ctx;
const category = input.category === "conferencing" ? "video" : input.category;
const category = input.category;
const localApps = getLocalAppMetadata();
const dbApps = await prisma.app.findMany({

View File

@ -3,7 +3,7 @@ import type { PrismaClient } from "@prisma/client";
import { getLocalAppMetadata } from "@calcom/app-store/utils";
import { sendDisabledAppEmail } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server";
import type { AppCategories } from "@calcom/prisma/enums";
import { AppCategories } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
@ -53,7 +53,13 @@ export const toggleHandler = async ({ input, ctx }: ToggleOptions) => {
// If disabling an app then we need to alert users basesd on the app type
if (!enabled) {
const translations = new Map();
if (app.categories.some((category) => ["calendar", "video"].includes(category))) {
if (
app.categories.some((category) =>
(
[AppCategories.calendar, AppCategories.video, AppCategories.conferencing] as AppCategories[]
).includes(category)
)
) {
// Find all users with the app credentials
const appCredentials = await prisma.credential.findMany({
where: {

View File

@ -21,6 +21,7 @@ declare namespace NodeJS {
readonly EMAIL_SERVER_USER: string | undefined;
readonly EMAIL_SERVER_PASSWORD: string | undefined;
readonly CRON_API_KEY: string | undefined;
readonly CRON_ENABLE_APP_SYNC: string | undefined;
readonly NEXT_PUBLIC_STRIPE_PUBLIC_KEY: string | undefined;
readonly STRIPE_PRIVATE_KEY: string | undefined;
readonly STRIPE_CLIENT_ID: string | undefined;

View File

@ -186,6 +186,7 @@
"CI",
"CLOSECOM_API_KEY",
"CRON_API_KEY",
"CRON_ENABLE_APP_SYNC",
"DAILY_API_KEY",
"DAILY_SCALE_PLAN",
"DEBUG",