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
3 changes: 0 additions & 3 deletions components/freshsales/.gitignore

This file was deleted.

2 changes: 0 additions & 2 deletions components/freshsales/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
106 changes: 106 additions & 0 deletions components/freshsales/actions/create-contact/create-contact.mjs
Original file line number Diff line number Diff line change
@@ -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;
},
Comment on lines +19 to +71
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Good dynamic property generation with type consistency fix needed.

The logic correctly handles the special sales_accounts field by splitting it into primary and additional account properties. However, there's the same type inconsistency issue as in the create-deal action.

Issue: Type inconsistency in multi_select_dropdown handling

Lines 27-29 set multi_select_dropdown fields to "string[]", but lines 60-65 override this to "integer" for dropdown fields. This creates inconsistent behavior for multi-select dropdowns.

Apply this fix to maintain type consistency:

        if ([
          "dropdown",
          "multi_select_dropdown",
        ].includes(field.type)) {
-         data.type = "integer";
+         data.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
          data.options = field.choices.map((choice) => ({
            label: choice.value,
            value: choice.id,
          }));
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
}
return props;
},
🤖 Prompt for AI Agents
In components/freshsales/actions/create-contact/create-contact.mjs between lines
19 and 71, the type for multi_select_dropdown fields is inconsistently set as
"string[]" initially but then changed to "integer" later in the dropdown
handling logic. To fix this, ensure that multi_select_dropdown fields
consistently use the same type by adjusting the type assignment inside the
dropdown type check to differentiate between single and multi-select dropdowns,
setting "integer" for dropdown and "integer[]" for multi_select_dropdown, and
update the options mapping accordingly to maintain type consistency.

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;
}
Comment on lines +79 to +96
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Address performance issue with delete operator.

The data transformation logic is correct but uses the delete operator which can impact performance. The static analysis tool correctly flagged this issue.

Apply this fix to improve performance by using undefined assignment instead of delete:

-     delete data.primaryAccount;
-     delete data.additionalAccounts;
+     data.primaryAccount = undefined;
+     data.additionalAccounts = undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
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,
},
);
});
data.primaryAccount = undefined;
data.additionalAccounts = undefined;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 94-94: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)


[error] 95-95: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

🤖 Prompt for AI Agents
In components/freshsales/actions/create-contact/create-contact.mjs around lines
79 to 96, replace the delete operator used on data.primaryAccount and
data.additionalAccounts with assignments to undefined to improve performance.
Instead of deleting these properties, set data.primaryAccount = undefined and
data.additionalAccounts = undefined after processing.


const response = await freshsales.createContact({
$,
data,
});

$.export("$summary", `Successfully created contact with ID: ${response.contact.id}`);
return response;
},
};
89 changes: 89 additions & 0 deletions components/freshsales/actions/create-deal/create-deal.mjs
Original file line number Diff line number Diff line change
@@ -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;
},
Comment on lines +18 to +73
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Good dynamic property generation with room for type consistency improvement.

The dynamic property generation logic correctly filters visible fields and handles special cases appropriately. However, there's a type inconsistency issue that should be addressed.

Issue: Type inconsistency in multi_select_dropdown handling

Lines 26-28 set multi_select_dropdown fields to "string[]", but lines 62-67 override this to "integer" for dropdown fields. This creates inconsistent behavior for multi-select dropdowns.

Apply this fix to maintain type consistency:

      if ([
        "dropdown",
        "multi_select_dropdown",
      ].includes(field.type)) {
-       data.type = "integer";
+       data.type = field.type === "multi_select_dropdown" ? "integer[]" : "integer";
        data.options = field.choices.map((choice) => ({
          label: choice.value,
          value: choice.id,
        }));
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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 = field.type === "multi_select_dropdown"
? "integer[]"
: "integer";
data.options = field.choices.map((choice) => ({
label: choice.value,
value: choice.id,
}));
}
props[field.name] = data;
}
return props;
},
🤖 Prompt for AI Agents
In components/freshsales/actions/create-deal/create-deal.mjs between lines 18
and 73, the type for multi_select_dropdown fields is initially set to "string[]"
but later overwritten to "integer" in the dropdown type handling block, causing
inconsistency. To fix this, modify the condition that sets data.type to
"integer" so it excludes multi_select_dropdown fields, ensuring
multi_select_dropdown fields retain the "string[]" type while only dropdown
fields get "integer".

async run({ $ }) {

const {
freshsales,
...data
} = this;

const response = await freshsales.createDeal({
$,
data,
});

$.export("$summary", `Successfully created deal with ID: ${response.deal.id}`);
return response;
},
};
Original file line number Diff line number Diff line change
@@ -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;
},
};
34 changes: 34 additions & 0 deletions components/freshsales/actions/list-all-deals/list-all-deals.mjs
Original file line number Diff line number Diff line change
@@ -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;
},
};
13 changes: 0 additions & 13 deletions components/freshsales/app/freshsales.app.ts

This file was deleted.

29 changes: 29 additions & 0 deletions components/freshsales/common/utils.mjs
Original file line number Diff line number Diff line change
@@ -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;
};
Loading
Loading