diff --git a/components/freshsales/.gitignore b/components/freshsales/.gitignore deleted file mode 100644 index ec761ccab7595..0000000000000 --- a/components/freshsales/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.js -*.mjs -dist \ No newline at end of file diff --git a/components/freshsales/README.md b/components/freshsales/README.md index e166995f3e2ce..f030a7ef378e6 100644 --- a/components/freshsales/README.md +++ b/components/freshsales/README.md @@ -4,8 +4,6 @@ The Freshsales API offers a suite of functionalities to enhance your CRM experie # Example Use Cases -- **Lead Scoring Automation**: Automatically update the lead score in Freshsales based on customer interactions tracked in other tools. For instance, increase a lead's score when they open a marketing email sent via SendGrid. - - **Deal Progression Notifications**: Set up a workflow that sends real-time Slack notifications to a sales channel when a deal moves to a new stage in the Freshsales pipeline, keeping the team instantly informed. - **Customer Success Handover**: Automate the process of creating a task in project management tools like Asana when a deal is won in Freshsales, ensuring smooth handover from sales to customer success teams. diff --git a/components/freshsales/actions/create-contact/create-contact.mjs b/components/freshsales/actions/create-contact/create-contact.mjs new file mode 100644 index 0000000000000..4e69282e3099f --- /dev/null +++ b/components/freshsales/actions/create-contact/create-contact.mjs @@ -0,0 +1,106 @@ +import { parseObject } from "../../common/utils.mjs"; +import freshsales from "../../freshsales.app.mjs"; + +export default { + key: "freshsales-create-contact", + name: "Create Contact", + description: "Create a new contact in your Freshsales account. [See the documentation](https://developer.freshsales.io/api/#create_a_contact)", + version: "0.0.1", + type: "action", + props: { + freshsales, + email: { + type: "string", + label: "Email", + description: "Email of the contact", + reloadProps: true, + }, + }, + async additionalProps() { + const { fields } = await this.freshsales.getContactFields(); + const filteredFields = fields.filter((field) => (field.visible === true && field.name != "emails")); + + const props = {}; + for (const field of filteredFields) { + + const data = { + type: field.type === "multi_select_dropdown" + ? "string[]" + : "string", + label: field.label, + description: `${field.label} of the contact.`, + optional: field.required === false, + }; + + if (field.name === "sales_accounts") { + const { sales_accounts: options } = await this.freshsales.getSalesAccounts(); + const salesAccountOptions = options.map((account) => ({ + label: account.name, + value: `${account.id}`, + })); + props.primaryAccount = { + type: "string", + label: "Primary Sales Account", + description: "Primary sales account of the contact.", + optional: true, + options: salesAccountOptions, + }; + props.additionalAccounts = { + type: "string[]", + label: "Additional Sales Accounts", + description: "Additional sales accounts of the contact.", + optional: true, + options: salesAccountOptions, + }; + } else { + if ([ + "dropdown", + "multi_select_dropdown", + ].includes(field.type)) { + data.type = "integer"; + data.options = field.choices.map((choice) => ({ + label: choice.value, + value: choice.id, + })); + } + props[field.name] = data; + } + } + + return props; + }, + async run({ $ }) { + + const { + freshsales, + ...data + } = this; + + if (data.primaryAccount) { + data.sales_accounts = [ + { + id: data.primaryAccount, + is_primary: true, + }, + ]; + parseObject(data.additionalAccounts)?.map((account) => { + data.sales_accounts.push( + { + id: account, + is_primary: false, + }, + ); + }); + delete data.primaryAccount; + delete data.additionalAccounts; + } + + const response = await freshsales.createContact({ + $, + data, + }); + + $.export("$summary", `Successfully created contact with ID: ${response.contact.id}`); + return response; + }, +}; diff --git a/components/freshsales/actions/create-deal/create-deal.mjs b/components/freshsales/actions/create-deal/create-deal.mjs new file mode 100644 index 0000000000000..be85df1a44ad8 --- /dev/null +++ b/components/freshsales/actions/create-deal/create-deal.mjs @@ -0,0 +1,89 @@ +import freshsales from "../../freshsales.app.mjs"; + +export default { + key: "freshsales-create-deal", + name: "Create Deal", + description: "Create a new deal in your Freshsales account. [See the documentation](https://developer.freshsales.io/api/#create_deal)", + version: "0.0.1", + type: "action", + props: { + freshsales, + name: { + type: "string", + label: "Name", + description: "Name of the deal", + reloadProps: true, + }, + }, + async additionalProps() { + const { fields } = await this.freshsales.getDealFields(); + const filteredFields = fields.filter((field) => (field.visible === true && field.name != "name")); + + const props = {}; + for (const field of filteredFields) { + + const data = { + type: field.type === "multi_select_dropdown" + ? "string[]" + : "string", + label: field.label, + description: `${field.label} of the deal.`, + optional: field.required === false, + }; + + if (field.name === "sales_account_id") { + const { sales_accounts: options } = await this.freshsales.getSalesAccounts(); + data.type = "integer"; + data.label = "Sales Account"; + data.description = "Sales account of the deal."; + data.optional = true; + data.options = options.map((account) => ({ + label: account.name, + value: account.id, + })); + } + + if (field.name === "contacts") { + const { contacts: options } = await this.freshsales.getContacts(); + data.type = "integer[]"; + data.label = "Contacts"; + data.description = "Contacts of the deal."; + data.optional = true; + data.options = options.map((contact) => ({ + label: contact.display_name, + value: contact.id, + })); + } + + if ([ + "dropdown", + "multi_select_dropdown", + ].includes(field.type)) { + data.type = "integer"; + data.options = field.choices.map((choice) => ({ + label: choice.value, + value: choice.id, + })); + } + + props[field.name] = data; + } + + return props; + }, + async run({ $ }) { + + const { + freshsales, + ...data + } = this; + + const response = await freshsales.createDeal({ + $, + data, + }); + + $.export("$summary", `Successfully created deal with ID: ${response.deal.id}`); + return response; + }, +}; diff --git a/components/freshsales/actions/list-all-contacts/list-all-contacts.mjs b/components/freshsales/actions/list-all-contacts/list-all-contacts.mjs new file mode 100644 index 0000000000000..f5f0012797300 --- /dev/null +++ b/components/freshsales/actions/list-all-contacts/list-all-contacts.mjs @@ -0,0 +1,34 @@ +import freshsales from "../../freshsales.app.mjs"; + +export default { + key: "freshsales-list-all-contacts", + name: "List All Contacts", + description: "Fetch all contacts from your Freshsales account. [See the documentation](https://developer.freshsales.io/api/#list_all_contacts)", + version: "0.0.1", + type: "action", + props: { + freshsales, + }, + async run({ $ }) { + const filterId = await this.freshsales.getFilterId({ + model: "contacts", + name: "All Contacts", + }); + + const response = this.freshsales.paginate({ + fn: this.freshsales.listContacts, + responseField: "contacts", + filterId, + }); + + const contacts = []; + for await (const contact of response) { + contacts.push(contact); + } + + $.export("$summary", `Successfully fetched ${contacts?.length || 0} contact${contacts?.length === 1 + ? "" + : "s"}`); + return contacts; + }, +}; diff --git a/components/freshsales/actions/list-all-deals/list-all-deals.mjs b/components/freshsales/actions/list-all-deals/list-all-deals.mjs new file mode 100644 index 0000000000000..7def995e27044 --- /dev/null +++ b/components/freshsales/actions/list-all-deals/list-all-deals.mjs @@ -0,0 +1,34 @@ +import freshsales from "../../freshsales.app.mjs"; + +export default { + key: "freshsales-list-all-deals", + name: "List All Deals", + description: "Fetch all deals from your Freshsales account. [See the documentation](https://developer.freshsales.io/api/#list_all_deals)", + version: "0.0.1", + type: "action", + props: { + freshsales, + }, + async run({ $ }) { + const filterId = await this.freshsales.getFilterId({ + model: "deals", + name: "All Deals", + }); + + const response = this.freshsales.paginate({ + fn: this.freshsales.listDeals, + responseField: "deals", + filterId, + }); + + const deals = []; + for await (const deal of response) { + deals.push(deal); + } + + $.export("$summary", `Successfully fetched ${deals?.length || 0} deal${deals?.length === 1 + ? "" + : "s"}`); + return deals; + }, +}; diff --git a/components/freshsales/app/freshsales.app.ts b/components/freshsales/app/freshsales.app.ts deleted file mode 100644 index 055f6b8721df7..0000000000000 --- a/components/freshsales/app/freshsales.app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineApp } from "@pipedream/types"; - -export default defineApp({ - type: "app", - app: "freshsales", - propDefinitions: {}, - methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); - }, - }, -}); diff --git a/components/freshsales/common/utils.mjs b/components/freshsales/common/utils.mjs new file mode 100644 index 0000000000000..b6cfa02119c82 --- /dev/null +++ b/components/freshsales/common/utils.mjs @@ -0,0 +1,29 @@ +export const snakeCaseToTitleCase = (s) => + s.replace(/^_*(.)|_+(.)/g, (s, c, d) => c + ? c.toUpperCase() + : " " + d.toUpperCase()); + +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/freshsales/freshsales.app.mjs b/components/freshsales/freshsales.app.mjs new file mode 100644 index 0000000000000..02d86148fdbbf --- /dev/null +++ b/components/freshsales/freshsales.app.mjs @@ -0,0 +1,170 @@ +import { axios } from "@pipedream/platform"; + +export default { + type: "app", + app: "freshsales", + propDefinitions: { + contactId: { + type: "integer", + label: "Contact ID", + description: "Select a contact or provide a contact ID", + async options() { + const response = await this.listContacts(); + return response.contacts?.map(({ + id, first_name, last_name, email, + }) => ({ + label: `${first_name || ""} ${last_name || ""}`.trim() || email || `Contact ${id}`, + value: id, + })) || []; + }, + }, + dealId: { + type: "integer", + label: "Deal ID", + description: "Select a deal or provide a deal ID", + async options() { + const response = await this.listDeals(); + return response.deals?.map(({ + id, name, + }) => ({ + label: name || `Deal ${id}`, + value: id, + })) || []; + }, + }, + ownerId: { + type: "integer", + label: "Owner ID", + description: "Select an owner or provide an owner ID", + async options() { + const response = await this._makeRequest({ + method: "GET", + url: "/users", + }); + return response.users?.map(({ + id, display_name, email, + }) => ({ + label: display_name || email || `User ${id}`, + value: id, + })) || []; + }, + }, + }, + methods: { + _headers() { + return { + "Authorization": `Token token=${this.$auth.api_key}`, + "Content-Type": "application/json", + }; + }, + _baseUrl() { + return `https://${this.$auth.bundle_alias}/api`; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._baseUrl()}${path}`, + headers: this._headers(), + ...opts, + }); + }, + async getFilterId({ + model, name, ...opts + }) { + const { filters } = await this._makeRequest({ + path: `/${model}/filters`, + ...opts, + }); + return filters.find((filter) => filter.name === name).id; + }, + listContacts({ + filterId, ...opts + }) { + return this._makeRequest({ + path: `/contacts/view/${filterId}`, + ...opts, + }); + }, + listDeals({ + filterId, ...opts + }) { + return this._makeRequest({ + path: `/deals/view/${filterId}`, + ...opts, + }); + }, + getContactFields(opts = {}) { + return this._makeRequest({ + path: "/settings/contacts/fields", + ...opts, + }); + }, + getDealFields(opts = {}) { + return this._makeRequest({ + path: "/settings/deals/fields", + ...opts, + }); + }, + async getSalesAccounts(opts = {}) { + const filterId = await this.getFilterId({ + model: "sales_accounts", + name: "All Accounts", + }); + return this._makeRequest({ + path: `/sales_accounts/view/${filterId}`, + ...opts, + }); + }, + async getContacts(opts = {}) { + const filterId = await this.getFilterId({ + model: "contacts", + name: "All Contacts", + }); + return this._makeRequest({ + path: `/contacts/view/${filterId}`, + ...opts, + }); + }, + createContact(args) { + return this._makeRequest({ + path: "/contacts", + method: "POST", + ...args, + }); + }, + createDeal(args) { + return this._makeRequest({ + path: "/deals", + method: "POST", + ...args, + }); + }, + async *paginate({ + fn, params = {}, responseField, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; + + do { + params.page = ++page; + const data = await fn({ + params, + ...opts, + }); + + for (const d of data[responseField]) { + yield d; + + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = data[responseField].length; + + } while (hasMore); + }, + }, +}; diff --git a/components/freshsales/package.json b/components/freshsales/package.json index 87c64f1fc7c26..3bd32bf323ba2 100644 --- a/components/freshsales/package.json +++ b/components/freshsales/package.json @@ -1,16 +1,18 @@ { "name": "@pipedream/freshsales", - "version": "0.0.2", + "version": "0.1.0", "description": "Pipedream Freshsales Components", - "main": "dist/app/freshsales.app.mjs", + "main": "freshsales.app.mjs", "keywords": [ "pipedream", "freshsales" ], - "files": ["dist"], "homepage": "https://pipedream.com/apps/freshsales", "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0572c97234a8d..ce7f933fa2e11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4984,7 +4984,11 @@ importers: specifier: ^3.0.0 version: 3.0.3 - components/freshsales: {} + components/freshsales: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.0 components/freshservice: dependencies: @@ -5971,8 +5975,7 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/hana: - specifiers: {} + components/hana: {} components/handwrytten: {}