Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/all-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/apps.browser.generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.keys-schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/crm.apps.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<AppCard
onAppInstallSuccess={onAppInstallSuccess}
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
app={app}
teamId={eventType.team?.id || undefined}
switchOnClick={(e) => {
updateEnabled(e);
}}
switchChecked={enabled}
hideAppCardOptions
/>
);
};

export default EventTypeAppCard;
16 changes: 16 additions & 0 deletions packages/app-store/crovecrm/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions packages/app-store/crovecrm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./lib/CrmService";
131 changes: 131 additions & 0 deletions packages/app-store/crovecrm/lib/CrmService.ts
Original file line number Diff line number Diff line change
@@ -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<CrmEvent | undefined> {
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,
})),
Comment on lines +25 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using event.attendees.map directly can lead to a runtime error if event.attendees is undefined or null. It is safer to use optional chaining and provide a fallback empty array, similar to how it is handled in deleteEvent.

Suggested change
attendees: event.attendees.map((a) => ({
email: a.email,
name: a.name,
timeZone: a.timeZone,
})),
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<CrmEvent> {
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,
})),
Comment on lines +50 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using event.attendees.map directly can lead to a runtime error if event.attendees is undefined or null. It is safer to use optional chaining and provide a fallback empty array, similar to how it is handled in deleteEvent.

Suggested change
attendees: event.attendees.map((a) => ({
email: a.email,
name: a.name,
timeZone: a.timeZone,
})),
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<void> {
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<Contact[]> {
return [];
}

async createContacts(
contactsToCreate: ContactCreateInput[],
_organizerEmail?: string
): Promise<Contact[]> {
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<EventBusyDate[]> {
return [];
}

async listCalendars(_event?: CalendarEvent): Promise<IntegrationCalendar[]> {
return [];
}
}

export default function BuildCrmService(
credential: CredentialPayload,
_appOptions?: Record<string, unknown>
): CRM {
return new CroveCrmIntegrationService(credential);
}
10 changes: 10 additions & 0 deletions packages/app-store/crovecrm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@calcom/crovecrm",
"version": "0.0.0",
"main": "./index.ts",
"types": "./index.ts",
"private": true,
"devDependencies": {
"@calcom/types": "workspace:*"
}
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The package imports from @calcom/features and @calcom/lib, but these workspace dependencies are not declared in package.json. To ensure correct Turbo task graph execution, caching, and dependency resolution, please add them to devDependencies.

  "devDependencies": {
    "@calcom/features": "workspace:*",
    "@calcom/lib": "workspace:*",
    "@calcom/types": "workspace:*"
  }

}
11 changes: 11 additions & 0 deletions packages/app-store/crovecrm/static/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/app-store/crovecrm/zod.ts
Original file line number Diff line number Diff line change
@@ -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;