diff --git a/.github/workflows/all-checks.yml b/.github/workflows/all-checks.yml index 6459fe9b31b..3d9325e43ac 100644 --- a/.github/workflows/all-checks.yml +++ b/.github/workflows/all-checks.yml @@ -11,72 +11,84 @@ concurrency: jobs: type-check: name: Type check + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/check-types.yml secrets: inherit lint: name: Linters + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/lint.yml secrets: inherit unit-test: name: Tests + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/unit-tests.yml secrets: inherit api-v2-unit-test: name: Tests + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/api-v2-unit-tests.yml secrets: inherit build-api-v2: name: Production builds + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/api-v2-production-build.yml secrets: inherit build-atoms: name: Production builds + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/atoms-production-build.yml secrets: inherit build: name: Production builds + if: github.repository == 'calcom/cal.diy' uses: ./.github/workflows/production-build-without-database.yml secrets: inherit integration-test: name: Tests + if: github.repository == 'calcom/cal.diy' needs: [lint, build, build-api-v2] uses: ./.github/workflows/integration-tests.yml secrets: inherit e2e: name: Tests + if: github.repository == 'calcom/cal.diy' needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e.yml secrets: inherit e2e-app-store: name: Tests + if: github.repository == 'calcom/cal.diy' needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-app-store.yml secrets: inherit e2e-embed: name: Tests + if: github.repository == 'calcom/cal.diy' needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-embed.yml secrets: inherit e2e-embed-react: name: Tests + if: github.repository == 'calcom/cal.diy' needs: [lint, build, build-api-v2] uses: ./.github/workflows/e2e-embed-react.yml secrets: inherit required: needs: [lint, type-check, unit-test, api-v2-unit-test, integration-test, build, build-api-v2, e2e, e2e-embed, e2e-embed-react, e2e-app-store] - if: always() + if: github.repository == 'calcom/cal.diy' && always() runs-on: ubuntu-latest steps: - name: fail if conditional jobs failed diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 51f315ec2f0..e5b0df21ec3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -436,7 +436,7 @@ jobs: e2e-embed-react, e2e-app-store, ] - if: always() + if: github.repository == 'calcom/cal.diy' && always() runs-on: ubuntu-latest steps: - name: Fail if trust-check did not succeed diff --git a/packages/app-store/apps.browser.generated.tsx b/packages/app-store/apps.browser.generated.tsx index cf6fef19fff..4c66c0fcfdd 100644 --- a/packages/app-store/apps.browser.generated.tsx +++ b/packages/app-store/apps.browser.generated.tsx @@ -23,6 +23,7 @@ export const EventTypeAddonMap = { basecamp3: dynamic(() => import("./basecamp3/components/EventTypeAppCardInterface")), btcpayserver: dynamic(() => import("./btcpayserver/components/EventTypeAppCardInterface")), closecom: dynamic(() => import("./closecom/components/EventTypeAppCardInterface")), + crovecrm: dynamic(() => import("./crovecrm/components/EventTypeAppCardInterface")), databuddy: dynamic(() => import("./databuddy/components/EventTypeAppCardInterface")), fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")), ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")), diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 43e1a41e4b7..7e18e6a35db 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -6,6 +6,7 @@ import { appKeysSchema as alby_zod_ts } from "./alby/zod"; import { appKeysSchema as basecamp3_zod_ts } from "./basecamp3/zod"; import { appKeysSchema as btcpayserver_zod_ts } from "./btcpayserver/zod"; import { appKeysSchema as closecom_zod_ts } from "./closecom/zod"; +import { appKeysSchema as crovecrm_zod_ts } from "./crovecrm/zod"; import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; import { appKeysSchema as databuddy_zod_ts } from "./databuddy/zod"; import { appKeysSchema as dub_zod_ts } from "./dub/zod"; @@ -57,6 +58,7 @@ export const appKeysSchemas = { basecamp3: basecamp3_zod_ts, btcpayserver: btcpayserver_zod_ts, closecom: closecom_zod_ts, + crovecrm: crovecrm_zod_ts, dailyvideo: dailyvideo_zod_ts, databuddy: databuddy_zod_ts, dub: dub_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index bb666932ded..b4e948723af 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -19,6 +19,7 @@ import clara_config_json from "./clara/config.json"; import clic_config_json from "./clic/config.json"; import closecom_config_json from "./closecom/config.json"; import cron_config_json from "./cron/config.json"; +import crovecrm_config_json from "./crovecrm/config.json"; import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; import databuddy_config_json from "./databuddy/config.json"; import deel_config_json from "./deel/config.json"; @@ -132,6 +133,7 @@ export const appStoreMetadata = { clic: clic_config_json, closecom: closecom_config_json, cron: cron_config_json, + crovecrm: crovecrm_config_json, dailyvideo: dailyvideo__metadata_ts, databuddy: databuddy_config_json, deel: deel_config_json, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 4085408dbeb..d3b8011ddfb 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -6,6 +6,7 @@ import { appDataSchema as alby_zod_ts } from "./alby/zod"; import { appDataSchema as basecamp3_zod_ts } from "./basecamp3/zod"; import { appDataSchema as btcpayserver_zod_ts } from "./btcpayserver/zod"; import { appDataSchema as closecom_zod_ts } from "./closecom/zod"; +import { appDataSchema as crovecrm_zod_ts } from "./crovecrm/zod"; import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; import { appDataSchema as databuddy_zod_ts } from "./databuddy/zod"; import { appDataSchema as dub_zod_ts } from "./dub/zod"; @@ -57,6 +58,7 @@ export const appDataSchemas = { basecamp3: basecamp3_zod_ts, btcpayserver: btcpayserver_zod_ts, closecom: closecom_zod_ts, + crovecrm: crovecrm_zod_ts, dailyvideo: dailyvideo_zod_ts, databuddy: databuddy_zod_ts, dub: dub_zod_ts, diff --git a/packages/app-store/crm.apps.generated.ts b/packages/app-store/crm.apps.generated.ts index 94392d5ee50..ac262dd84d7 100644 --- a/packages/app-store/crm.apps.generated.ts +++ b/packages/app-store/crm.apps.generated.ts @@ -4,6 +4,7 @@ **/ export const CrmServiceMap = { closecom: import("./closecom/lib/CrmService"), + crovecrm: import("./crovecrm/lib/CrmService"), hubspot: import("./hubspot/lib/CrmService"), "pipedrive-crm": import("./pipedrive-crm/lib/CrmService"), salesforce: import("./salesforce/lib/CrmService"), diff --git a/packages/app-store/crovecrm/components/EventTypeAppCardInterface.tsx b/packages/app-store/crovecrm/components/EventTypeAppCardInterface.tsx new file mode 100644 index 00000000000..4fd338c3516 --- /dev/null +++ b/packages/app-store/crovecrm/components/EventTypeAppCardInterface.tsx @@ -0,0 +1,26 @@ +import { usePathname } from "next/navigation"; +import AppCard from "@calcom/app-store/_components/AppCard"; +import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; +import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; +import { WEBAPP_URL } from "@calcom/lib/constants"; + +const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType, onAppInstallSuccess }) { + const pathname = usePathname(); + const { enabled, updateEnabled } = useIsAppEnabled(app); + + return ( + { + updateEnabled(e); + }} + switchChecked={enabled} + hideAppCardOptions + /> + ); +}; + +export default EventTypeAppCard; diff --git a/packages/app-store/crovecrm/config.json b/packages/app-store/crovecrm/config.json new file mode 100644 index 00000000000..0fa1f1b7ab1 --- /dev/null +++ b/packages/app-store/crovecrm/config.json @@ -0,0 +1,16 @@ +{ + "name": "Crove CRM", + "slug": "crovecrm", + "type": "crovecrm_crm", + "logo": "icon.svg", + "url": "https://crm.crove.com", + "variant": "crm", + "categories": ["crm"], + "extendsFeature": "EventType", + "publisher": "MetaDOS LLC", + "email": "help@crove.com", + "description": "Automatically synchronize booked appointments, attendee contacts, and meeting timelines directly into Crove CRM with Organization and Team attribution.", + "isTemplate": false, + "__createdUsingCli": true, + "dirName": "crovecrm" +} diff --git a/packages/app-store/crovecrm/index.ts b/packages/app-store/crovecrm/index.ts new file mode 100644 index 00000000000..0e90fe5bd34 --- /dev/null +++ b/packages/app-store/crovecrm/index.ts @@ -0,0 +1 @@ +export * from "./lib/CrmService"; diff --git a/packages/app-store/crovecrm/lib/CrmService.ts b/packages/app-store/crovecrm/lib/CrmService.ts new file mode 100644 index 00000000000..23821a0c576 --- /dev/null +++ b/packages/app-store/crovecrm/lib/CrmService.ts @@ -0,0 +1,131 @@ +import { CroveCrmService } from "@calcom/features/crove-crm"; +import type { CalendarEvent, EventBusyDate, IntegrationCalendar } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; +import type { CRM, Contact, ContactCreateInput, CrmEvent } from "@calcom/types/CrmService"; + +export class CroveCrmIntegrationService implements CRM { + private crm: CroveCrmService; + private credential: CredentialPayload; + + constructor(credential: CredentialPayload) { + this.credential = credential; + const key = credential.key as { api_key?: string; api_url?: string } | undefined; + this.crm = new CroveCrmService(key?.api_key, key?.api_url); + } + + async createEvent(event: CalendarEvent, _contacts?: Contact[]): Promise { + const result = await this.crm.syncBookingEvent({ + triggerEvent: "BOOKING_CREATED", + payload: { + uid: event.uid || undefined, + title: event.title, + startTime: event.startTime, + endTime: event.endTime, + organizer: event.organizer, + attendees: event.attendees.map((a) => ({ + email: a.email, + name: a.name, + timeZone: a.timeZone, + })), + }, + }); + + return { + id: event.uid || `crovecrm_${Date.now()}`, + uid: event.uid || undefined, + type: "crovecrm", + additionalInfo: result, + }; + } + + async updateEvent(uid: string, event: CalendarEvent): Promise { + const result = await this.crm.syncBookingEvent({ + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + uid, + title: event.title, + startTime: event.startTime, + endTime: event.endTime, + organizer: event.organizer, + attendees: event.attendees.map((a) => ({ + email: a.email, + name: a.name, + timeZone: a.timeZone, + })), + }, + }); + + return { + id: uid, + uid, + type: "crovecrm", + additionalInfo: result, + }; + } + + async deleteEvent(uid: string, event: CalendarEvent): Promise { + await this.crm.syncBookingEvent({ + triggerEvent: "BOOKING_CANCELLED", + payload: { + uid, + title: event?.title || "Meeting", + startTime: event?.startTime || new Date().toISOString(), + endTime: event?.endTime, + organizer: event?.organizer || { email: "host@crove.com" }, + attendees: event?.attendees?.map((a) => ({ + email: a.email, + name: a.name, + timeZone: a.timeZone, + })) || [], + }, + }); + } + + async getContacts(_options?: { emails: string | string[]; includeOwner?: boolean }): Promise { + return []; + } + + async createContacts( + contactsToCreate: ContactCreateInput[], + _organizerEmail?: string + ): Promise { + const created: Contact[] = []; + for (const c of contactsToCreate) { + if (c.email) { + const res = await this.crm.upsertContact({ + email: c.email, + name: c.name, + phone: c.phone || undefined, + }); + created.push({ + id: res.contactId || c.email, + email: c.email, + }); + } + } + return created; + } + + getAppOptions() { + return {}; + } + + async getAvailability( + _dateFrom: string, + _dateTo: string, + _selectedCalendars: IntegrationCalendar[] + ): Promise { + return []; + } + + async listCalendars(_event?: CalendarEvent): Promise { + return []; + } +} + +export default function BuildCrmService( + credential: CredentialPayload, + _appOptions?: Record +): CRM { + return new CroveCrmIntegrationService(credential); +} diff --git a/packages/app-store/crovecrm/package.json b/packages/app-store/crovecrm/package.json new file mode 100644 index 00000000000..2ffadc518eb --- /dev/null +++ b/packages/app-store/crovecrm/package.json @@ -0,0 +1,10 @@ +{ + "name": "@calcom/crovecrm", + "version": "0.0.0", + "main": "./index.ts", + "types": "./index.ts", + "private": true, + "devDependencies": { + "@calcom/types": "workspace:*" + } +} diff --git a/packages/app-store/crovecrm/static/icon.svg b/packages/app-store/crovecrm/static/icon.svg new file mode 100644 index 00000000000..2b4835c4956 --- /dev/null +++ b/packages/app-store/crovecrm/static/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/app-store/crovecrm/zod.ts b/packages/app-store/crovecrm/zod.ts new file mode 100644 index 00000000000..7b284a739bb --- /dev/null +++ b/packages/app-store/crovecrm/zod.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +import { eventTypeAppCardZod } from "../eventTypeAppCardZod"; + +export const appKeysSchema = z.object({ + api_key: z.string().optional(), + api_url: z.string().optional(), +}); + +export const appDataSchema = eventTypeAppCardZod;