From 29bd74feeab59b89c499e31be7f9af3a146bc91b Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 10 Oct 2024 15:32:06 +0200 Subject: [PATCH] 7203 support emails links phones in zapier inputs 2 (#7562) ## Done - add `EMAILS`, `PHONES`, `LINKS`, `RICH_TEXT`, `POSITION`, and `ARRAY` field support in Twenty zapier integration - fix `twenty-zapier` package tests and requirements ## Emails image ## Links image ## Phones image ## Array image --- packages/twenty-zapier/jest.config.ts | 9 ++ packages/twenty-zapier/package.json | 1 + .../src/test/utils/computeInputFields.test.ts | 53 ++++++-- .../src/test/utils/handleQueryParams.test.ts | 19 ++- .../src/utils/computeInputFields.ts | 120 ++++++++++++++++-- .../twenty-zapier/src/utils/data.types.ts | 19 ++- .../src/utils/handleQueryParams.ts | 18 ++- packages/twenty-zapier/tsconfig.json | 9 +- yarn.lock | 3 +- 9 files changed, 221 insertions(+), 30 deletions(-) create mode 100644 packages/twenty-zapier/jest.config.ts diff --git a/packages/twenty-zapier/jest.config.ts b/packages/twenty-zapier/jest.config.ts new file mode 100644 index 000000000..39406fb49 --- /dev/null +++ b/packages/twenty-zapier/jest.config.ts @@ -0,0 +1,9 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js'], + transformIgnorePatterns: ['/node_modules/'], +}; diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json index a87732489..966ffe6e6 100644 --- a/packages/twenty-zapier/package.json +++ b/packages/twenty-zapier/package.json @@ -25,6 +25,7 @@ "convertedByCLIVersion": "15.4.1" }, "dependencies": { + "dotenv": "^16.4.5", "zapier-platform-core": "15.5.1" }, "devDependencies": { diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts index 3eb1433cd..4d2d4600a 100644 --- a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts +++ b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts @@ -1,5 +1,5 @@ import { computeInputFields } from '../../utils/computeInputFields'; -import { InputField } from '../../utils/data.types'; +import { FieldMetadataType, InputField } from '../../utils/data.types'; describe('computeInputFields', () => { test('should create Person input fields properly', () => { @@ -11,7 +11,7 @@ describe('computeInputFields', () => { edges: [ { node: { - type: 'RELATION', + type: FieldMetadataType.RELATION, name: 'favorites', label: 'Favorites', description: 'Favorites linked to the contact', @@ -21,7 +21,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'CURRENCY', + type: FieldMetadataType.CURRENCY, name: 'annualSalary', label: 'Annual Salary', description: 'Annual Salary of the Person', @@ -31,7 +31,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'jobTitle', label: 'Job Title', description: 'Contact’s job title', @@ -43,7 +43,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'DATE_TIME', + type: FieldMetadataType.DATE_TIME, name: 'updatedAt', label: 'Update date', description: null, @@ -55,7 +55,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'FULL_NAME', + type: FieldMetadataType.FULL_NAME, name: 'name', label: 'Name', description: 'Contact’s name', @@ -68,7 +68,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'UUID', + type: FieldMetadataType.UUID, name: 'id', label: 'Id', description: null, @@ -81,7 +81,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'NUMBER', + type: FieldMetadataType.NUMBER, name: 'recordPosition', label: 'RecordPosition', description: 'Record Position', @@ -91,7 +91,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'LINK', + type: FieldMetadataType.LINK, name: 'xLink', label: 'X', description: 'Contact’s X/Twitter account', @@ -101,7 +101,17 @@ describe('computeInputFields', () => { }, { node: { - type: 'EMAIL', + type: FieldMetadataType.LINKS, + name: 'whatsapp', + label: 'Whatsapp', + description: 'Contact’s Whatsapp account', + isNullable: true, + defaultValue: null, + }, + }, + { + node: { + type: FieldMetadataType.EMAIL, name: 'email', label: 'Email', description: 'Contact’s Email', @@ -113,7 +123,7 @@ describe('computeInputFields', () => { }, { node: { - type: 'UUID', + type: FieldMetadataType.UUID, name: 'companyId', label: 'Company id (foreign key)', description: 'Contact’s company id foreign key', @@ -190,6 +200,27 @@ describe('computeInputFields', () => { helpText: 'Contact’s X/Twitter account: Link Label', required: false, }, + { + key: 'whatsapp__url', + label: 'Whatsapp: Url', + type: 'string', + helpText: 'Contact’s Whatsapp account: Link Url', + required: false, + }, + { + key: 'whatsapp__label', + label: 'Whatsapp: Label', + type: 'string', + helpText: 'Contact’s Whatsapp account: Link Label', + required: false, + }, + { + key: 'whatsapp__secondaryLinks', + label: 'Whatsapp: Secondary Lings', + type: 'string', + helpText: 'Contact’s Whatsapp account: Link Label', + required: false, + }, { key: 'email', label: 'Email', diff --git a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts index 872879900..d01fb2b38 100644 --- a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts +++ b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts @@ -14,6 +14,20 @@ describe('utils.handleQueryParams', () => { domainName: 'Company Domain Name', linkedinUrl__url: '/linkedin_url', linkedinUrl__label: 'Test linkedinUrl', + whatsapp__primaryLinkUrl: '/whatsapp_url', + whatsapp__primaryLinkLabel: 'Whatsapp Link', + whatsapp__secondaryLinks: [ + "{url: '/secondary_whatsapp_url',label: 'Secondary Whatsapp Link'}", + ], + emails: { + primaryEmail: 'primary@email.com', + additionalEmails: ['secondary@email.com'], + }, + phones: { + primaryPhoneNumber: '322110011', + primaryPhoneCountryCode: '+33', + additionalPhones: ["{ phoneNumber: '322110012', countryCode: '+33' }"], + }, xUrl__url: '/x_url', xUrl__label: 'Test xUrl', annualRecurringRevenue: 100000, @@ -23,9 +37,12 @@ describe('utils.handleQueryParams', () => { const result = handleQueryParams(inputData); const expectedResult = 'name: "Company Name", ' + - 'address: { addressCity: "Paris" }, ' + + 'address: {addressCity: "Paris"}, ' + 'domainName: "Company Domain Name", ' + 'linkedinUrl: {url: "/linkedin_url", label: "Test linkedinUrl"}, ' + + 'whatsapp: {primaryLinkUrl: "/whatsapp_url", primaryLinkLabel: "Whatsapp Link", secondaryLinks: [{url: \'/secondary_whatsapp_url\',label: \'Secondary Whatsapp Link\'}]}, ' + + 'emails: {primaryEmail: "primary@email.com", additionalEmails: ["secondary@email.com"]}, ' + + 'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'+33\' }]}, ' + 'xUrl: {url: "/x_url", label: "Test xUrl"}, ' + 'annualRecurringRevenue: 100000, ' + 'idealCustomerProfile: true, ' + diff --git a/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts index f2eabc331..dbc24cc13 100644 --- a/packages/twenty-zapier/src/utils/computeInputFields.ts +++ b/packages/twenty-zapier/src/utils/computeInputFields.ts @@ -5,15 +5,21 @@ import { NodeField, } from '../utils/data.types'; +const getListFromFieldMetadataType = (fieldMetadataType: FieldMetadataType) => { + return fieldMetadataType === FieldMetadataType.ARRAY; +}; + const getTypeFromFieldMetadataType = ( - fieldMetadataType: string, + fieldMetadataType: FieldMetadataType, ): string | undefined => { switch (fieldMetadataType) { case FieldMetadataType.UUID: case FieldMetadataType.TEXT: + case FieldMetadataType.RICH_TEXT: case FieldMetadataType.PHONE: case FieldMetadataType.EMAIL: case FieldMetadataType.LINK: + case FieldMetadataType.ARRAY: case FieldMetadataType.RATING: return 'string'; case FieldMetadataType.DATE_TIME: @@ -23,6 +29,7 @@ const getTypeFromFieldMetadataType = ( case FieldMetadataType.BOOLEAN: return 'boolean'; case FieldMetadataType.NUMBER: + case FieldMetadataType.POSITION: return 'integer'; case FieldMetadataType.NUMERIC: return 'number'; @@ -35,7 +42,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { switch (nodeField.type) { case FieldMetadataType.FULL_NAME: { const firstName: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'firstName', label: 'First Name', description: 'First Name', @@ -43,7 +50,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const lastName: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'lastName', label: 'Last Name', description: 'Last Name', @@ -54,7 +61,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { } case FieldMetadataType.LINK: { const url: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'url', label: 'Url', description: 'Link Url', @@ -62,7 +69,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const label: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'label', label: 'Label', description: 'Link Label', @@ -73,7 +80,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { } case FieldMetadataType.CURRENCY: { const amountMicros: NodeField = { - type: 'NUMBER', + type: FieldMetadataType.NUMBER, name: 'amountMicros', label: 'Amount Micros', description: 'Amount Micros. eg: set 3210000 for 3.21$', @@ -81,7 +88,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const currencyCode: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'currencyCode', label: 'Currency Code', description: 'Currency Code. eg: USD, EUR, etc...', @@ -92,7 +99,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { } case FieldMetadataType.ADDRESS: { const address1: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressStreet1', label: 'Address', description: 'Address', @@ -100,7 +107,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const address2: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressStreet2', label: 'Address 2', description: 'Address 2', @@ -108,7 +115,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const city: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressCity', label: 'City', description: 'City', @@ -116,7 +123,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const state: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressState', label: 'State', description: 'State', @@ -124,7 +131,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const postalCode: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressPostalCode', label: 'Postal Code', description: 'Postal Code', @@ -132,7 +139,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { defaultValue: null, }; const country: NodeField = { - type: 'TEXT', + type: FieldMetadataType.TEXT, name: 'addressCountry', label: 'Country', description: 'Country', @@ -141,6 +148,84 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => { }; return [address1, address2, city, state, postalCode, country]; } + case FieldMetadataType.PHONES: { + const primaryPhoneNumber: NodeField = { + type: FieldMetadataType.TEXT, + name: 'primaryPhoneNumber', + label: 'Primary Phone Number', + description: 'Primary Phone Number. 600112233', + isNullable: true, + defaultValue: null, + }; + const primaryPhoneCountryCode: NodeField = { + type: FieldMetadataType.TEXT, + name: 'primaryPhoneCountryCode', + label: 'Primary Phone Country Code', + description: 'Primary Phone Country Code. eg: +33', + isNullable: true, + defaultValue: null, + }; + const additionalPhones: NodeField = { + type: FieldMetadataType.TEXT, + name: 'additionalPhones', + label: 'Additional Phones', + description: 'Additional Phones', + isNullable: true, + defaultValue: null, + placeholder: '{ number: "", countryCode: "" }', + list: true, + }; + return [primaryPhoneNumber, primaryPhoneCountryCode, additionalPhones]; + } + case FieldMetadataType.EMAILS: { + const primaryEmail: NodeField = { + type: FieldMetadataType.TEXT, + name: 'primaryEmail', + label: 'Primary Email', + description: 'Primary Email', + isNullable: true, + defaultValue: null, + }; + const additionalEmails: NodeField = { + type: FieldMetadataType.TEXT, + name: 'additionalEmails', + label: 'Additional Emails', + description: 'Additional Emails', + list: true, + isNullable: true, + defaultValue: null, + }; + return [primaryEmail, additionalEmails]; + } + case FieldMetadataType.LINKS: { + const primaryLinkLabel: NodeField = { + type: FieldMetadataType.TEXT, + name: 'primaryLinkLabel', + label: 'Primary Link Label', + description: 'Primary Link Label', + isNullable: true, + defaultValue: null, + }; + const primaryLinkUrl: NodeField = { + type: FieldMetadataType.TEXT, + name: 'primaryLinkUrl', + label: 'Primary Link Url', + description: 'Primary Link Url', + isNullable: true, + defaultValue: null, + }; + const secondaryLinks: NodeField = { + type: FieldMetadataType.TEXT, + name: 'secondaryLinks', + label: 'Secondary Links', + description: 'Secondary Links', + isNullable: true, + defaultValue: null, + placeholder: '{ url: "", label: "" }', + list: true, + }; + return [primaryLinkLabel, primaryLinkUrl, secondaryLinks]; + } default: throw new Error(`Unknown nodeField type: ${nodeField.type}`); } @@ -161,6 +246,9 @@ export const computeInputFields = ( case FieldMetadataType.FULL_NAME: case FieldMetadataType.LINK: case FieldMetadataType.CURRENCY: + case FieldMetadataType.PHONES: + case FieldMetadataType.EMAILS: + case FieldMetadataType.LINKS: case FieldMetadataType.ADDRESS: for (const subNodeField of get_subfieldsFromField(nodeField)) { const field = { @@ -169,12 +257,15 @@ export const computeInputFields = ( type: getTypeFromFieldMetadataType(subNodeField.type), helpText: `${nodeField.description}: ${subNodeField.description}`, required: isFieldRequired(subNodeField), + list: !!subNodeField.list, + placeholder: subNodeField.placeholder, } as InputField; result.push(field); } break; case FieldMetadataType.UUID: case FieldMetadataType.TEXT: + case FieldMetadataType.RICH_TEXT: case FieldMetadataType.PHONE: case FieldMetadataType.EMAIL: case FieldMetadataType.DATE_TIME: @@ -182,6 +273,8 @@ export const computeInputFields = ( case FieldMetadataType.BOOLEAN: case FieldMetadataType.NUMBER: case FieldMetadataType.NUMERIC: + case FieldMetadataType.POSITION: + case FieldMetadataType.ARRAY: case FieldMetadataType.RATING: { const nodeFieldType = getTypeFromFieldMetadataType(nodeField.type); if (!nodeFieldType) { @@ -196,6 +289,7 @@ export const computeInputFields = ( type: nodeFieldType, helpText: nodeField.description, required, + list: getListFromFieldMetadataType(nodeField.type), }; result.push(field); break; diff --git a/packages/twenty-zapier/src/utils/data.types.ts b/packages/twenty-zapier/src/utils/data.types.ts index ba73ae18e..9c6b35d33 100644 --- a/packages/twenty-zapier/src/utils/data.types.ts +++ b/packages/twenty-zapier/src/utils/data.types.ts @@ -3,12 +3,14 @@ export type InputData = { [x: string]: any }; export type ObjectData = { id: string } | { [x: string]: any }; export type NodeField = { - type: string; + type: FieldMetadataType; name: string; label: string; description: string | null; isNullable: boolean; defaultValue: object | null; + list?: boolean; + placeholder?: string; }; export type Node = { @@ -28,26 +30,39 @@ export type InputField = { type: string; helpText: string | null; required: boolean; + list?: boolean; + placeholder?: string; }; export enum FieldMetadataType { UUID = 'UUID', TEXT = 'TEXT', PHONE = 'PHONE', + PHONES = 'PHONES', EMAIL = 'EMAIL', + EMAILS = 'EMAILS', DATE_TIME = 'DATE_TIME', DATE = 'DATE', BOOLEAN = 'BOOLEAN', NUMBER = 'NUMBER', NUMERIC = 'NUMERIC', LINK = 'LINK', + LINKS = 'LINKS', CURRENCY = 'CURRENCY', FULL_NAME = 'FULL_NAME', RATING = 'RATING', SELECT = 'SELECT', MULTI_SELECT = 'MULTI_SELECT', - RELATION = 'RELATION', + POSITION = 'POSITION', ADDRESS = 'ADDRESS', + RICH_TEXT = 'RICH_TEXT', + ARRAY = 'ARRAY', + + // Ignored fieldTypes + RELATION = 'RELATION', + RAW_JSON = 'RAW_JSON', + ACTOR = 'ACTOR', + TS_VECTOR = 'TS_VECTOR', } export type Schema = { diff --git a/packages/twenty-zapier/src/utils/handleQueryParams.ts b/packages/twenty-zapier/src/utils/handleQueryParams.ts index 5bbb58815..15039e9e3 100644 --- a/packages/twenty-zapier/src/utils/handleQueryParams.ts +++ b/packages/twenty-zapier/src/utils/handleQueryParams.ts @@ -1,5 +1,17 @@ import { InputData } from '../utils/data.types'; +const OBJECT_SUBFIELD_NAMES = ['secondaryLinks', 'additionalPhones']; + +const formatArrayInputData = ( + key: string, + arrayInputData: InputData, +): string => { + if (OBJECT_SUBFIELD_NAMES.includes(key)) { + return `${arrayInputData[key].join('","')}`; + } + return `"${arrayInputData[key].join('","')}"`; +}; + const handleQueryParams = (inputData: InputData): string => { const formattedInputData: InputData = {}; Object.keys(inputData).forEach((key) => { @@ -17,7 +29,11 @@ const handleQueryParams = (inputData: InputData): string => { let result = ''; Object.keys(formattedInputData).forEach((key) => { let quote = ''; - if (typeof formattedInputData[key] === 'object') { + if (Array.isArray(formattedInputData[key])) { + result = result.concat( + `${key}: [${formatArrayInputData(key, formattedInputData)}], `, + ); + } else if (typeof formattedInputData[key] === 'object') { result = result.concat( `${key}: {${handleQueryParams(formattedInputData[key])}}, `, ); diff --git a/packages/twenty-zapier/tsconfig.json b/packages/twenty-zapier/tsconfig.json index 1a3eb323e..cebedee14 100644 --- a/packages/twenty-zapier/tsconfig.json +++ b/packages/twenty-zapier/tsconfig.json @@ -9,5 +9,12 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true - } + }, + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "jest.config.ts" + ] } diff --git a/yarn.lock b/yarn.lock index a2066d9d3..025638bc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24321,7 +24321,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0": +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0, dotenv@npm:^16.4.5": version: 16.4.5 resolution: "dotenv@npm:16.4.5" checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f @@ -43852,6 +43852,7 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-zapier@workspace:packages/twenty-zapier" dependencies: + dotenv: "npm:^16.4.5" jest: "npm:29.7.0" rimraf: "npm:^3.0.2" zapier-platform-cli: "npm:^15.4.1"