Detect language used in a HubSpot property /
with Operations Hub

In the world of customer relationship management, HubSpot has emerged as a powerhouse for managing and automating customer interactions. However, when dealing with multilingual content and global audiences, it's crucial to employ tools that can identify the language of your customers effectively. One such tool is Operations Hub pro with this language detection Custom Coded Action, which can be seamlessly integrated into HubSpot workflows to enhance customer communication and content personalization.

Demo

How language detect works in HubSpot ?

Text_LanguageDetect is a PHP library renowned for its language detection capabilities. Although natively designed for PHP, you can harness its power within your HubSpot workflows using a custom code block in JavaScript. Let’s explore how this library can benefit your HubSpot environment.

Language Identification

The primary feature of This Custom Coded Action is its accurate language identification. In a CRM like HubSpot, where interactions occur in multiple languages, this Custom Coded Action can automatically categorize and sort text content based on language. This facilitates more targeted and relevant communication, ensuring that your customers receive information in their preferred language.

Personalized Customer Communication

HubSpot’s strength lies in personalizing customer interactions. By incorporating this Custom Coded Action you can take personalization to the next level. Create workflows that adapt based on detected language, sending emails, responses, or content in the language your customers prefer. This enhancement improves the overall customer experience, making it more engaging and satisfying.

Language-Specific Automation

Use this Custom Coded Action to trigger automation based on language. For instance, you can set up workflows that route customer inquiries to language-specific support teams. This ensures that your customers receive support and information in their preferred language, enhancing engagement and satisfaction.

Integration with HubSpot Workflows

Integrating this Custom Coded action into your HubSpot workflows is seamless using the custom code block feature. This integration empowers you to create dynamic, language-aware processes within your CRM.

This Custom Coded action is a powerful tool for enhancing your HubSpot CRM when dealing with multilingual content and global customer interactions. By accurately identifying languages, personalizing customer communication, automating language-specific processes, you can provide a more tailored and effective customer experience.

Deploying Language Detection Code in HubSpot

Follow these steps to integrate the language detection functionality into your CRM:

1. Create a Blank Workflow

Start by creating a new workflow in your HubSpot account. This workflow will be the foundation for implementing the language detection code.

2. Create a Trigger with a Filter

Within your workflow, establish a trigger with a filter to specify when language detection should occur. For instance, if you want to detect the language based on a description, you can set a filter such as « Description is known. » This filter will determine when this custom code will be executed.

3. Add a Custom Code Block

To integrate language detection into your workflow, you’ll need to use a custom code block. This is where you’ll include the JavaScript code responsible for language detection.

4. Include Properties in the Code

Within the custom code block, you should define the properties you want to analyze. For example, if you’re detecting language based on a product description, you might use a variable like textToAnalyze to represent the description text.

5. Copy Code from the Repository

To make this integration even more straightforward, we’ve provided a repository with pre-compiled code for language detection. You can access this repository at github Detect-language.

Inside the repository, navigate to the dist folder and copy the contents. These pre-compiled files contain the necessary code to execute language detection using this custom code.

6. Set Output Variables

In your custom code block, you need to specify the output variables and their types. For language detection, you typically want to capture the detected language and the confidence level. Define the following variables:

This step ensures that the detected language and the confidence score are made available for use in your HubSpot workflow.

By following these steps, you’ll seamlessly integrate language detection into your HubSpot workflow, enabling you to automate language detection and enhance your CRM’s capabilities for personalized customer communication and content organization.

The source code

Do not copy this code, it’s the source code use the code in the /dist folder of this repo : github Detect-language.

const LanguageDetect = require('languagedetect');

exports.main = async (event, callback) => {


    const textToAnalyze = event.inputFields.textToAnalyze;

    if (!textToAnalyze) throw new Error('textToAnalyze is not set, are you sure you put textToAnalyze in the "properties to include in code" ? ');


    const lngDetector = new LanguageDetect();

    const languageFound = lngDetector.detect(textToAnalyze,2);

    if(languageFound.length === 0) throw new Error('We failed to indentify the language');

    const [language, confidance]= languageFound[0];

    if(!language) throw new Error('Error when identifing the language');

    if(!confidance) throw new Error('Error when identifing the language no confidance data');


    callback({
        outputFields: {
            language,
            confidance
        }
    });

}

What is HubSpot-OperationHub-cca-compiler?

HubSpot-OperationHub-cca-compiler is a local development tool that provides a framework for working on your Custom Coded Actions (CCAs) locally and executing them in the same context as HubSpot. With this tool, you can write your CCAs using the libraries you want, even if they are not officially supported by HubSpot.

Demo

Key Features

Here are some of the key features and benefits of HubSpot-OperationHub-cca-compiler:

1. Library Freedom

Want to create a Custom Coded Action to perform a specific task, like validating phone number formats? Instead of writing all the logic from scratch, you can look for open-source modules and libraries online. However, Custom Coded Actions don’t allow you to import external libraries directly. HubSpot-OperationHub-cca-compiler offers a solution to this limitation. You can install the libraries you need locally, and the tool will package everything into a single file that works seamlessly within HubSpot.

2. Easy Setup

Using HubSpot-OperationHub-cca-compiler is straightforward. Start by cloning the project and installing the necessary dependencies with a simple command:

git clone https://github.com/Antoinebr/HubSpot-OperationHub-cca-compiler.git
cd HubSpot-OperationHub-cca-compiler
npm install

Create a .env file with your private App Token, which should look like this:

privateAppToken = "your-private-app-token"

3. Project Initialization

To create a new Custom Coded Action project, you can use the following command:

npm run init <nameOfYourProject>

For example:

npm run init my-new-custom-coded-action

This command generates a template that contains a file named cca.js. This is where you write your custom code.

4. Code Your CCA

Inside cca.js, you can write your CCA logic. The template provides a clear structure for your code, making it easy to follow. Here’s an example of a CCA that formats phone numbers:

const format = require('@sturdynut/i18n-phone-formatter');

exports.main = async (event, callback) => {
    const phoneNumber = event.inputFields.phoneNumber;

    if (!phoneNumber) throw new Error('phoneNumber is not set, are you sure you put phoneNumber in the "properties to include in code"?');

    const countryCode = event.inputFields.countryCode;

    if (!countryCode) throw an Error('countryCode is not set, are you sure you put country in the "properties to include in code"?');

    const formattedNumber = format.formatE164(countryCode, phoneNumber);

    callback({
        outputFields: {
            formattedNumber
        }
    });
}

The event.js file represents the properties you can include in your code, making it easy to interact with the data.

5. Node Modules

You can install any node modules you require for your project. Install them with npm, and then use them within your code:

npm install @sturdynut/i18n-phone-formatter

And then add it to your code :

const format = require('@sturdynut/i18n-phone-formatter');

6. Build and Deploy

After writing your CCA, build the project by running the following command:

npm run build <nameOfTheFolder>

This command creates a dist/ folder inside your project, which contains the compiled code.

Unfortunately, there’s no automated deployment for the compiled code. You will need to manually copy and paste the contents of dist/index.js into the Custom Coded Action block in HubSpot.

Conclusion

HubSpot-OperationHub-cca-compiler is a valuable tool for anyone looking to overcome limitations in HubSpot’s Operations Hub. It empowers you to work with the libraries you need, providing greater flexibility in your custom coding projects. With an easy setup process and well-structured templates, this tool streamlines the development and deployment of Custom Coded Actions, enhancing your capabilities within HubSpot.

Concept

See how it works / and implement it

PeopleDataLabs API key

You need to get an API key on the PeopleDataLabs website’s.

You need to set the peopleDataLabsAPI key in the secret, the API key has to be set in the secret section of the Custom Coded Action. Use the name peopleDataLabsAPI

Set the domain name variable

Set the variable name domainName in the property to include in code.

Code

Paste the following code :


const axios = require('axios'); exports.main = async (event, callback) => { if (!process.env.peopleDataLabsAPI) throw new Error('The peopleDataLabs API key has to be set in the secret section'); const domainName = event.inputFields.domainName; if (!domainName) throw new Error('domainName is not set, are you sure you put domainName in the "properties to include in code" ? '); const websiteInfos = await getWebsiteInfos(domainName).catch(axiosErrorHandler) if (!websiteInfos.data) throw new Error(`We couldn't grab your websiteInfos infos`); const { name, size, employee_count, id, founded, industry, linkedin_id, linkedin_url, facebook_url, twitter_url, summary } = websiteInfos.data; callback({ outputFields: { name, size, employee_count, id, founded, industry, linkedin_id, linkedin_url, facebook_url, twitter_url, summary } }); } /** * Retrieves information about a website's associated company using the PeopleDataLabs API. * * @async * @function * @param {string} domainName - The domain name of the website for which you want to fetch information. * @throws {Error} Throws an error if domainName is not a string or if it is empty. * @returns {Promise<Object>} A Promise that resolves to an object containing information about the company. * @see {@link https://docs.peopledatalabs.com/|PeopleDataLabs API Documentation} */ const getWebsiteInfos = async (domainName) => { if (typeof domainName !== 'string' || domainName.trim() === '') throw new Error('Invalid domainName parameter. It must be a non-empty string.'); // Construct the API endpoint URL with the provided domainName and API key. const endpoint = `https://api.peopledatalabs.com/v5/company/enrich?api_key=${process.env.peopleDataLabsAPI}&pretty=True&website=${domainName}`; // Make a GET request to the API and return the result. return axios.get(endpoint); } /** * Handles errors thrown by axios requests and logs relevant information. * * @param {Error} e - The error object thrown by axios. */ const axiosErrorHandler = e => { console.log(e); //console.log("The : ", e.config.method, " call on", e.config.url, "failed"); console.log("error code retuned ", e.code); console.log("error data returned ", e.response.data); }

Set the output / to use the data in the WorkFlow

name,
size,
employee_count,
id,
founded,
industry,
linkedin_id,
linkedin_url,
facebook_url,
twitter_url,
summary

Like so :

Then don’t forget to finish the logic with copy property values blocks to update the company record.

The logic / to find the firstname and gender

The idea is to split an email by a delimiter if one exists, then search in a huge first names list for a match.

Demo / see the logic in action

Take a look at this video to see the logic in action

Implement / in your portal

Follow this video to implement this Custom Coded Action

The code / ready to be used

Here’s where you will find the code :

https://github.com/Antoinebr/HubSpot-OperationHub-cca-compiler/blob/main/firstname-detector/dist/index.js

The source / code

Here’s the logic I created, but do not use this code in the Custom Coded Action ! Use the link above.
( This code is just the source code requires a extra build step ( Webpack ) ) this is why you can’t use that code directly.

const firstnames = require('./firstnames.js');

exports.main = async (event, callback) => {

    const email = event.inputFields.email;
    if (!email) throw new Error('email is not set, are you sure you put email in the "properties to include in code" ? ');

    const emailWithoutDomain = email.split('@')[0];


    const firstNameAndLastName = splitString(emailWithoutDomain);

    if(firstNameAndLastName.length === 1) throw new Error(`${email} can't be splitted by a separator`);

    const chunks = splitString(emailWithoutDomain).map(chunk => capitalizeString(chunk));

    const firstNameFound = [];

    for (const chunk of chunks) {

        if (firstnames[chunk]) {
            firstNameFound.push({
                firstName: chunk,
                gender: firstnames[chunk].gender
            });
        }
    }

    if(firstNameFound.length === 0) throw new Error('The firstName was not in the database');


    callback({
        outputFields: {
            gender: firstNameFound.length > -1 ? firstNameFound[0].gender : "N/A",
            firstName: firstNameFound.length > -1 ? firstNameFound[0].firstName : "N/A"
        }
    });

}


/**
 * Splits a string into an array of parts based on delimiters such as commas, dashes, underscores, dots, and forward slashes.
 *
 * @param {string} str - The string to be split.
 * @returns {array} - An array of the split string parts.
 */
const splitString = (str) => {
    return str.split(/[-_.]+/);
}

exports.splitString = splitString;


/**
 * Capitalizes the first character of a string.
 *
 * @param {string} str - The string to capitalize.
 * @returns {string} - The capitalized string.
 */
const capitalizeString = (str) => {
    return str.charAt(0).toUpperCase() + str.slice(1);
}
exports.capitalizeString = capitalizeString;

The logic we want to create

As you can see the deal is renamed based on the existing deals

Prerequisites

The logic

How to / tutorial

In this video I explain the concept, then I show you how to code the logic and how to implement it.

The code

Here’s the final code which you can implement in your portal :

/***
 *  SET THE SEPARTOR 
 *  Here set the separator 
 *  If your deals name are Deal-RQ001 then set "-RQ" as a sepator 
 *  The code will read the latest deal
 */

const separator = "-RQ";





const axios = require('axios');

const axiosConfig = {
    headers: {
        authorization: `Bearer ${process.env.privateAppToken}`
    }
};

exports.main = async (event, callback) => {

    if (!separator || separator === "") throw new Error('separator is not set, are you sure you put separator in the code ? ( see first lines )');

    const dealName = event.inputFields.dealName;

    if (!dealName) throw new Error('dealName is not set, are you sure you put dealName in the "properties to include in code" ? ');

    const deals = await searchDealsWithSeparator(separator).catch(axiosErrorHandler);

    console.log(deals.data.results[0].properties.dealname);

    const latestDealName = deals.data.results[0].properties.dealname;


    if (!latestDealName.includes(separator)) throw new Error("The deal doesn't contain the right separator")

    const numericValueAfterSeparator = latestDealName.split(separator)[1]


    const newNumericValue = parseInt(numericValueAfterSeparator) + 1;


    callback({
        outputFields: {
            newDealName: `${dealName}${separator}${newNumericValue}`
        }
    });

}



const getLastestsDeals = async () => {

    const endpoint = `https://api.hubapi.com/deals/v1/deal/recent/`;

    return axios.get(endpoint, axiosConfig);
}


const searchDealsWithSeparator = async (separator) => {

    const endpoint = `https://api.hubapi.com/crm/v3/objects/deals/search`;

    return axios.post(endpoint, {
        "filterGroups": [{
            "filters": [{
                "value": `*${separator}*`,
                "propertyName": "dealname",
                "operator": "CONTAINS_TOKEN"
            }]
        }],
        "properties": [
            "hubspot_owner_id",
            "dealname",
            "name"
        ],
        "sorts": [{
            "propertyName": "id",
            "direction": "DESCENDING"
        }],
        "limit": 1,
        //"after": after
    }, axiosConfig);

}
/**
 * Handles errors thrown by axios requests and logs relevant information.
 *
 * @param {Error} e - The error object thrown by axios.
 */
const axiosErrorHandler = e => {
    console.log("The : ", e.config.method, " call on", e.config.url, "failed");
    console.log("error code retuned ", e.code);
    console.log("error data returned ", e.response.data);
}

Enrichir son CRM HubSpot en se basant sur le SIRET

Le registre du commerce à beaucoup d’informations sur vos clients, le SIRET peut vous aider à les récupérer et enrichir votre CRM.

Depuis le numéro SIRET, vous pouvez notamment récupérer la raison sociale exacte d’une entreprise son effectif ou encore son adresse postale.

Concept et démonstration

Concept de l’appel à une API via Operations Hub

Le principe est assez simple, un WorkFlow utilisera un block Custom Code pour envoyer une requête à l’API de l’INSEE pour demander des infos.
Si l’API de l’INSEE renvoie les informations, alors nous les sauvegardons dans l’objet entreprise. Rien de plus simple.

Démonstration de l’intégration

Mieux qu’un long discours, j’ai enregistré une courte vidéo qui montre cette intégration en fonctionnement. Dans cette video je vais montrer les informations que renvoie l’API à HubSpot.

Pourquoi ne peut on pas trouver le numéro SIRET d’une entreprise ?

En fait l’API SIREN le propose, mais le soucis se trouve sur la clé de recherche.Je m’explique, si vous cherchez un numéro de SIRET en se basant sur nom de l’entreprise, vous allez avoir énormement de réponses… Et que faire avec 25, 30… SIRET, comment décider celui qui correspond à l’entreprise que vous avez dans le CRM ?.. C’est compliqué, il faudrait rafiner la data.
De ce fait je n’aborderais pas ce point dans l’article.

Prérequis

Pour mettre en place cette intégration, il vous faudra Operations Hub pro, car nous allons utiliser le block custom code. Vous allez aussi avoir besoin de créer un compte sur le site de l’INSEE pour récupérer un accès à l’API.

Il va aussi vous fallour une propriété SIREN dans vos entrerprises ou contacts

Créer un compte api.insee.fr

Il vous faut un compte INSEE, rendez-vous sur cette URL pour ce faire.

Cliquez sur créer son compte

Remplissez le formulaire :

Souscrivez à l’API SIREN

Générez un jeton d’accès

J’ai mis en place un jeton d’accès qui n’expire jamais en metant la valeur la plus élevée possible, j’ai mis : 3875820019684211 secondes.

Mettre en place le workflow

Créez un WorkFlow qui se base sur entreprise ou autre.

Le trigger du WorkFlow

Installez le custom code

Séléctionnez le language

Ici notre Custom Coded Action est écrite en JavaScript il vous faut donc séléctionner Node.js comme language.

Insérez votre jetons d’accès comme secret

Nommez votre secret apiInseeToken attention de en pas rajouter ou d’enlever des majuscules ou des espaces.

Ajoutez une variable SIRET

La variable siret correspond à la porpriété qui cotient le numéro SIRET.

Ajoutez-la comme ce-ci :

Collez le code suivant :


const axios = require('axios'); const axiosConfig = { headers: { authorization: `Bearer ${process.env.apiInseeToken}` } }; exports.main = async (event, callback) => { const siret = event.inputFields.siret; if (!siret) throw new Error('siret is not set, are you sure you put siret in the "properties to include in code" ? '); const siretInfos = await getInfosFromSiret(siret).catch(axiosErrorHandler) if (!siretInfos.data) throw new Error(`We couldn't grab your siretInfos`); /** * * Etablissement DATA * **/ const { etablissement } = siretInfos.data; if (!etablissement) throw new Error(`Couldn't get the etablissement data`); const { siren, nic, statutDiffusionEtablissement, dateCreationEtablissement, trancheEffectifsEtablissement, anneeEffectifsEtablissement, activitePrincipaleRegistreMetiersEtablissement, dateDernierTraitementEtablissement, etablissementSiege, nombrePeriodesEtablissement, } = etablissement; /** * * Etablissement Unite Legale * **/ let etatAdministratifUniteLegale = null; let statutDiffusionUniteLegale = null; let dateCreationUniteLegale = null; let categorieJuridiqueUniteLegale = null; let denominationUniteLegale = null; let sigleUniteLegale = null; let denominationUsuelle1UniteLegale = null; let denominationUsuelle2UniteLegale = null; let denominationUsuelle3UniteLegale = null; let sexeUniteLegale = null; let nomUniteLegale = null; let nomUsageUniteLegale = null; let prenom1UniteLegale = null; let prenom2UniteLegale = null; let prenom3UniteLegale = null; let prenom4UniteLegale = null; let prenomUsuelUniteLegale = null; let pseudonymeUniteLegale = null; let activitePrincipaleUniteLegale = null; let nomenclatureActivitePrincipaleUniteLegale = null; let identifiantAssociationUniteLegale = null; let economieSocialeSolidaireUniteLegale = null; let societeMissionUniteLegale = null; let caractereEmployeurUniteLegale = null; let trancheEffectifsUniteLegale = null; let anneeEffectifsUniteLegale = null; let nicSiegeUniteLegale = null; let dateDernierTraitementUniteLegale = null; let anneeCategorieEntreprise = null; if (typeof etablissement.uniteLegale !== "undefined") { etatAdministratifUniteLegale = etablissement.uniteLegale.etatAdministratifUniteLegale; statutDiffusionUniteLegale = etablissement.uniteLegale.statutDiffusionUniteLegale; dateCreationUniteLegale = etablissement.uniteLegale.dateCreationUniteLegale; categorieJuridiqueUniteLegale = etablissement.uniteLegale.categorieJuridiqueUniteLegale; denominationUniteLegale = etablissement.uniteLegale.denominationUniteLegale; sigleUniteLegale = etablissement.uniteLegale.sigleUniteLegale; denominationUsuelle1UniteLegale = etablissement.uniteLegale.denominationUsuelle1UniteLegale; denominationUsuelle2UniteLegale = etablissement.uniteLegale.denominationUsuelle2UniteLegale; denominationUsuelle3UniteLegale = etablissement.uniteLegale.denominationUsuelle3UniteLegale; sexeUniteLegale = etablissement.uniteLegale.sexeUniteLegale; nomUniteLegale = etablissement.uniteLegale.nomUniteLegale; nomUsageUniteLegale = etablissement.uniteLegale.nomUsageUniteLegale; prenom1UniteLegale = etablissement.uniteLegale.prenom1UniteLegale; prenom2UniteLegale = etablissement.uniteLegale.prenom2UniteLegale; prenom3UniteLegale = etablissement.uniteLegale.prenom3UniteLegale; prenom4UniteLegale = etablissement.uniteLegale.prenom4UniteLegale; prenomUsuelUniteLegale = etablissement.uniteLegale.prenomUsuelUniteLegale; pseudonymeUniteLegale = etablissement.uniteLegale.pseudonymeUniteLegale; activitePrincipaleUniteLegale = etablissement.uniteLegale.activitePrincipaleUniteLegale; nomenclatureActivitePrincipaleUniteLegale = etablissement.uniteLegale.nomenclatureActivitePrincipaleUniteLegale; identifiantAssociationUniteLegale = etablissement.uniteLegale.identifiantAssociationUniteLegale; economieSocialeSolidaireUniteLegale = etablissement.uniteLegale.economieSocialeSolidaireUniteLegale; societeMissionUniteLegale = etablissement.uniteLegale.societeMissionUniteLegale; caractereEmployeurUniteLegale = etablissement.uniteLegale.caractereEmployeurUniteLegale; trancheEffectifsUniteLegale = etablissement.uniteLegale.trancheEffectifsUniteLegale; anneeEffectifsUniteLegale = etablissement.uniteLegale.anneeEffectifsUniteLegale; nicSiegeUniteLegale = etablissement.uniteLegale.nicSiegeUniteLegale; dateDernierTraitementUniteLegale = etablissement.uniteLegale.dateDernierTraitementUniteLegale; anneeCategorieEntreprise = etablissement.uniteLegale.anneeCategorieEntreprise; } /* * * Adresse Etablissement * */ let complementAdresseEtablissement = null; let numeroVoieEtablissement = null; let indiceRepetitionEtablissement = null; let typeVoieEtablissement = null; let libelleVoieEtablissement = null; let codePostalEtablissement = null; let libelleCommuneEtablissement = null; let libelleCommuneEtrangerEtablissement = null; let distributionSpecialeEtablissement = null; let codeCommuneEtablissement = null; let codeCedexEtablissement = null; let libelleCedexEtablissement = null; let codePaysEtrangerEtablissement = null; let libellePaysEtrangerEtablissement = null; if (typeof etablissement.adresseEtablissement !== "undefined") { complementAdresseEtablissement = etablissement.adresseEtablissement.complementAdresseEtablissement; numeroVoieEtablissement = etablissement.adresseEtablissement.numeroVoieEtablissement; indiceRepetitionEtablissement = etablissement.adresseEtablissement.indiceRepetitionEtablissement; typeVoieEtablissement = etablissement.adresseEtablissement.typeVoieEtablissement; libelleVoieEtablissement = etablissement.adresseEtablissement.libelleVoieEtablissement; codePostalEtablissement = etablissement.adresseEtablissement.codePostalEtablissement; libelleCommuneEtablissement = etablissement.adresseEtablissement.libelleCommuneEtablissement; libelleCommuneEtrangerEtablissement = etablissement.adresseEtablissement.libelleCommuneEtrangerEtablissement; distributionSpecialeEtablissement = etablissement.adresseEtablissement.distributionSpecialeEtablissement; codeCommuneEtablissement = etablissement.adresseEtablissement.codeCommuneEtablissement; codeCedexEtablissement = etablissement.adresseEtablissement.codeCedexEtablissement; libelleCedexEtablissement = etablissement.adresseEtablissement.libelleCedexEtablissement; codePaysEtrangerEtablissement = etablissement.adresseEtablissement.codePaysEtrangerEtablissement; libellePaysEtrangerEtablissement = etablissement.adresseEtablissement.libellePaysEtrangerEtablissement; } /* * * Adresse Etablissement 2 * */ let complementAdresse2Etablissement = null; let numeroVoie2Etablissement = null; let indiceRepetition2Etablissement = null; let typeVoie2Etablissement = null; let libelleVoie2Etablissement = null; let codePostal2Etablissement = null; let libelleCommune2Etablissement = null; let libelleCommuneEtranger2Etablissement = null; let distributionSpeciale2Etablissement = null; let codeCommune2Etablissement = null; let codeCedex2Etablissement = null; let libelleCedex2Etablissement = null; let codePaysEtranger2Etablissement = null; let libellePaysEtranger2Etablissement = null; if (typeof etablissement.adresse2Etablissement !== "undefined") { complementAdresse2Etablissement = etablissement.adresse2Etablissement.complementAdresse2Etablissement; numeroVoie2Etablissement = etablissement.adresse2Etablissement.numeroVoie2Etablissement; indiceRepetition2Etablissement = etablissement.adresse2Etablissement.indiceRepetition2Etablissement; typeVoie2Etablissement = etablissement.adresse2Etablissement.typeVoie2Etablissement; libelleVoie2Etablissement = etablissement.adresse2Etablissement.libelleVoie2Etablissement; codePostal2Etablissement = etablissement.adresse2Etablissement.codePostal2Etablissement; libelleCommune2Etablissement = etablissement.adresse2Etablissement.libelleCommune2Etablissement; libelleCommuneEtranger2Etablissement = etablissement.adresse2Etablissement.libelleCommuneEtranger2Etablissement; distributionSpeciale2Etablissement = etablissement.adresse2Etablissement.distributionSpeciale2Etablissement; codeCommune2Etablissement = etablissement.adresse2Etablissement.codeCommune2Etablissement; codeCedex2Etablissement = etablissement.adresse2Etablissement.codeCedex2Etablissement; libelleCedex2Etablissement = etablissement.adresse2Etablissement.libelleCedex2Etablissement; codePaysEtranger2Etablissement = etablissement.adresse2Etablissement.codePaysEtranger2Etablissement; libellePaysEtranger2Etablissement = etablissement.adresse2Etablissement.libellePaysEtranger2Etablissement; } callback({ outputFields: { siren, nic, siret, statutDiffusionEtablissement, dateCreationEtablissement, trancheEffectifsEtablissement, anneeEffectifsEtablissement, activitePrincipaleRegistreMetiersEtablissement, dateDernierTraitementEtablissement, etablissementSiege, nombrePeriodesEtablissement, etatAdministratifUniteLegale, statutDiffusionUniteLegale, dateCreationUniteLegale, categorieJuridiqueUniteLegale, denominationUniteLegale, sigleUniteLegale, denominationUsuelle1UniteLegale, denominationUsuelle2UniteLegale, denominationUsuelle3UniteLegale, sexeUniteLegale, nomUniteLegale, nomUsageUniteLegale, prenom1UniteLegale, prenom2UniteLegale, prenom3UniteLegale, prenom4UniteLegale, prenomUsuelUniteLegale, pseudonymeUniteLegale, activitePrincipaleUniteLegale, nomenclatureActivitePrincipaleUniteLegale, identifiantAssociationUniteLegale, economieSocialeSolidaireUniteLegale, societeMissionUniteLegale, caractereEmployeurUniteLegale, trancheEffectifsUniteLegale, anneeEffectifsUniteLegale, nicSiegeUniteLegale, dateDernierTraitementUniteLegale, anneeCategorieEntreprise, complementAdresseEtablissement, numeroVoieEtablissement, indiceRepetitionEtablissement, typeVoieEtablissement, libelleVoieEtablissement, codePostalEtablissement, libelleCommuneEtablissement, libelleCommuneEtrangerEtablissement, distributionSpecialeEtablissement, codeCommuneEtablissement, codeCedexEtablissement, libelleCedexEtablissement, codePaysEtrangerEtablissement, libellePaysEtrangerEtablissement, complementAdresse2Etablissement, numeroVoie2Etablissement, indiceRepetition2Etablissement, typeVoie2Etablissement, libelleVoie2Etablissement, codePostal2Etablissement, libelleCommune2Etablissement, libelleCommuneEtranger2Etablissement, distributionSpeciale2Etablissement, codeCommune2Etablissement, codeCedex2Etablissement, libelleCedex2Etablissement, codePaysEtranger2Etablissement, libellePaysEtranger2Etablissement } }); } const getInfosFromSiret = async (siret) => { if (!siret || siret === "") throw new Error('Siret is missing'); const endpoint = `https://api.insee.fr/entreprises/sirene/V3/siret/${siret}`; return axios.get(endpoint, axiosConfig); } const searchSiret = async (query) => { const endpoint = `https://api.insee.fr/entreprises/sirene/V3/siren?q=periode(denominationUniteLegale:${query})`; return axios.get(endpoint, axiosConfig); } /** * Handles errors thrown by axios requests and logs relevant information. * * @param {Error} e - The error object thrown by axios. */ const axiosErrorHandler = e => { console.log("The : ", e.config.method, " call on", e.config.url, "failed"); console.log("error code retuned ", e.code); console.log("error data returned ", e.response.data); }

Configurez la sortie ( output function )

Sauvegarder le resultat dans le CRM

Pour sauvegarder les infos dans la fiche entreprise il suffit d’utiliser le block copy property value rien de plus simple. Il suffit de deux clics pour convertir ce que l’on a configuré dan le « data output » en propriété du CRM.

Sentiment analysis demo

The fastest way to understand what you can achieve with this automation is to watch this short video.

The concept / schema

The concept is pretty simple, first we get the message from a user, then we create a Workflow which will listen new tickets.
This Workflow sends the content of the message to an API, then the API returns the sentiment.

Then we save the sentiment in the ticket.

How to set it up

In this part, I will explain how to create the workflow.

The Workflow trigger

Here’s the Workflow trigger we use is ticket description is known, so when we have a message to analyze.

Send the message to the API

Register to the API and grab an API key

We’re going to use the API Layer sentiment analysis API.
First go on this link and register, then find the Sentiment Analysis API

API layer Results

Take the free plan as you can analyze up to 1000 messages per month.

Then Get the API key :

Set up the code in the custom coded action

To send the message to the API, we use the custom code block, so create one.

For the language pick Node.js :

Then add a secret with the name : apiLayerKey , so click on add secret and paste the API key you got from API layer as the value.

Then add the message you want to analyze here, in my example I would like to analyze the Ticket description so in the section property to include in code I pick the ticket property Ticket description

On the left of the dropdown we have to name our property, put the name : textToAnalyze do not change the casing and do not add extra white spaces.

Paste the code :

In the code section remove everything then paste the following code :


const axios = require('axios'); const axiosConfig = { headers: { apikey: process.env.apiLayerKey } }; exports.main = async (event, callback) => { const textToAnalyze = event.inputFields.textToAnalyze; if (!textToAnalyze) throw new Error('textToAnalyze is not set, are you sure you put textToAnalyze in the "properties to include in code" ? '); const analysis = await analyzeSentiment(textToAnalyze).catch(axiosErrorHandler) if (!analysis.data) throw new Error(`We couldn't grab the analysis infos`); const { sentiment, language, confidence } = analysis.data; callback({ outputFields: { sentiment, language, confidence } }); } const analyzeSentiment = body => { if (!body) throw new Error(`There's no body`); const endpoint = `https://api.apilayer.com/sentiment/analysis`; return axios.post(endpoint, body, axiosConfig); } /** * Handles errors thrown by axios requests and logs relevant information. * * @param {Error} e - The error object thrown by axios. */ const axiosErrorHandler = e => { console.log("The : ", e.config.method, " call on", e.config.url, "failed"); console.log("error code retuned ", e.code); console.log("error data returned ", e.response.data); }

Add the output

The Custom Coded Action outputs’ is the data returned by the block to the workflow. In other words, the data other workflow blocks will be capable to use.

So in the output section add the following with the appropriate types (the order doesn’t matter)

string : sentiment
string : language
number : confidence

Test the logic

Now you can save the block, before that you can also give it a try by analyzing one of the existing ticket

Save the result in the ticket

To save the result in the ticket, nothing easier, just use the copy property value block.

Use the sentiment value for the automation

Starting from here, you have the sentiment in your workflow, a so lot of automation can be run based on this. Why not sending an internal message to the contact owner ? This is the power of the workflows, so the automations are up to you.

Write your first Custom Coded Action

In this article I’m will guide you and how to create your first Custom Coded Action. If you are not a developer this article is for you. I will explain without too much sideways how to create your first Custom Coded Action.

Let’s get started

Anatomy of a Custom Coded Action

First create a Workflow in my example I created one based on contact.

Then click on the plus sign to add a new block.

Select the Custom Code block

The code

Let’s take a look at the default code provided by HubSpot :

Variables

To use a custom property in your code, you have to select it in the « property to include in code » section.

Here I selected two contact properties : Email and carPlate to select the properties you want to use in your code click on the dropdown.

Then on the fields on the left you can set the variable name you want. Here I ketp the default names.

Then to use it in your code you have to use the following notation :

To access the email address in your code use :

const email = event.inputFields['email'];

Now, the contact’s email address will be set in the email variable.
The const email =parts means we store the email address in the email variable

Alternatively, you can use the dot notation like so :

const email = event.inputFields.email;

The keyword const is used most of the time to declare a variable, but you can also use the let keyword.

Like so :

let email = event.inputFields.email

The difference between const and let is : const cannot be reassigned, but let can.

Example :

This doesn’t work :

const email = event.inputFields.email: 
email = "antoine@gmail.com";

But if you use let instead it will work

let email = event.inputFields.email: 
email = "antoine@gmail.com";

In a nutshell:

« const » is for things that won’t change – once you put something in that box, it stays that way. « let » is for things that might change – you can put something in the box and later replace it with something else.

So, when you’re picking between « const » and « let, » think about whether you want a locked box that won’t change or a flexible box that you can update whenever you need to.

Display / return value from our code

To display something from our code you can use console.log() which is a function.

Think of console.log like a talking mirror that helps you understand what’s happening inside your program:

// The talking mirror (console.log)
let message = "Hello, magical mirror!";
console.log(message); // The mirror responds by showing the message

let number = 42;
console.log("The answer to everything is:", number); // The mirror responds again

So in our Custom Coded Action if I want to display the email address or / and the car plate my code will look like this :

Then if I click Test

The result will look like this :

Here’s the code you can use to test this

exports.main = async (event, callback) => {

  const email = event.inputFields.email;

  console.log("Email is ", email);

  callback({
    outputFields: {
      email: email
    }
  });
}

Return data to the workflow

But this only display the value in the Logs section, if you want to return the data to the WorkFlow and use that date in an other block. You have to use the callBack function.

This is useful if for example to save the result of the Custom Coded Action in the contact

To return the data in the WorkFlow you have to use the callback

  callback({
    outputFields: {
      email: email
    }
  });

This part of the code will return the email value to the WorkFlow, then it will be accessible to other blocks.

Like so :

If you click save in the top right corner, now the email variable will be accessible to other blocks.

I use the set property value block and can select the email from the Custom Coded Action.

IF statements

An « if statement » in JavaScript is like a decision-making tool in your everyday life. Imagine you’re deciding whether to go outside or stay indoors based on the weather. The « if statement » in JavaScript helps your program make similar decisions based on certain conditions.

Here’s a simple way to understand it:

Imagine you’re a robot programmed to make decisions based on weather conditions:

If it’s sunny: You’ll decide to go for a walk outside.
If it’s rainy: You’ll decide to stay indoors and read a book

In JavaScript, an « if statement » works similarly:

let weather = "sunny"; // You can change this value to "rainy" or something else

let activity;

if (weather === "sunny") {
    activity = "go for a walk";
} else if (weather === "rainy") {
    activity = "stay indoors and read a book";
} else {
    activity = "not sure what to do";
}

console.log("Today's weather is " + weather + ". I will " + activity + ".");

Functions

A function in JavaScript is similar to the functions you use in Excel or Google spreadsheet.

As an example, imagine you have a « Greet » machine that takes a name as input and gives you a greeting as output:

In JavaScript code, it might look like this:

// This is the "Greet" machine (function)
function greetFriend(name) {
    let greeting = "Hello, " + name + "!";
    return greeting;
}

// You press the button (call the function) and provide your friend's name
let friendName = "Alice";
let greetingMessage = greetFriend(friendName); // The machine gives you a greeting

console.log(greetingMessage); // You see the greeting: "Hello, Alice!"

Let’s use this function in a Custom Code :

exports.main = async (event, callback) => {

  const email = event.inputFields['email'];

  const firstName = event.inputFields.firstname;

  function greetFriend(name){

    const greeting = "Hello, "+name+" !"; // Hello, Antoine !

    return greeting;
  }

  const functionResult = greetFriend(firstName);

  console.log(functionResult);

  callback({
    outputFields: {
      email: email
    }
  });


}

Create and use a JavaScript function with ChatGPT

It can be faster and easier to create a fuction with chatGPT here’s how I created a function to test if a car is a preimum one or not.

The function :

function isPremiumCar(carBrand) {
    const premiumCarBrands = [
        'Audi', 'BMW', 'Bentley', 'Jaguar', 'Land Rover', 'Lexus', 'Maserati', 'Mercedes', 'Porsche', 'Rolls-Royce', 'Tesla', // Add more premium brands here
    ];

    return premiumCarBrands.includes(carBrand);
}

The complete Operations Hub custom code :

exports.main = async (event, callback) => {


  const carManufacturer = event.inputFields.carmanufacturer;

  function isPremiumCar(carBrand) {
    const premiumCarBrands = [
        'Audi', 'BMW', 'Bentley', 'Jaguar', 'Land Rover', 'Lexus', 'Maserati', 'Mercedes', 'Porsche', 'Rolls-Royce', 'Tesla', // Add more premium brands here
    ];

    return premiumCarBrands.includes(carBrand);
}


  const isCarPremium = isPremiumCar(carManufacturer);

  callback({
    outputFields: {
     isCarPremium
    }
  });


}

A chat GPT prompt you can use :


Context : serverless function developement Help Needed : I want you to help me to write a logic with JavaScript Constrains : the code should we written in this format : exports.main = async (event, callback) => { // code here callback({ outputFields: { } }); } The logic should be written inside the main function. If the logic returns data the data should be returned by the callback function inside the outputFields object. When you get external data input as parameters. The data comes from event.inputFields object. E.g : you get the contact email adress then the email variable should be set like : const email = event.inputFields.email; logic to implement :

Conclusion

In this article I gave you dome basic knowledge to get started with Custom Coded Action in HubSpot. However to use the Custom Coded Action at their full potential you need solid JavaScript fudenmental. If you are interested I definitely recomend this training :

What HubSpot gives us by default

On the platform in a Workflow, if you add a Custom Coded Action block, you have an interface where you can select the programming language you would like to use. A dropdown to manage / use your API secrets etc…

Underneath, you can write your code, but in that small editor there’s no autocompletion, you can’t format automatically your code. You can’t automatically write doc blocks for your functions…

That’s why I prefer to write my code locally in my IDE, that the way I produce better code, and I can also keep track of my changes with Git.

If you are interested to set up something similar, I will explain in this article how to set up a Custom Coded Action enviroment locally.

A JavaScript CLI to re-create the environment

The main idea is to have a local environment which provides the same functions and limitation as HubSpot. That’s why I created a CLI where I re-created the functions which are unique to the HubSpot Custom Coded Actions.
With this tool you can use your own IDE and feel like home.

prerequisites

Install the CLI

To install the CLI clone the following repository :

git clone https://github.com/Antoinebr/HubSpot-Operations-Hub-Custom-Coded-Action.git

Then install all the dependencies

cd HubSpot-Operations-Hub-Custom-Coded-Action
npm install 

Start a new project

To start a new project type :

npm run init my-new-custom-coded-action

This will copy the folder __TEMPLATE__ which is a scaffolding of a Custom Coded Action

Understand the unique functions and variables

In this video, I will explain some specific functions and variable used in the Custom Coded Actions. Nothing complicated, but still very important.

Manage the secrets in HubSpot

In a Custom Coded Action you can manage your secrets, which is ideal to store an API key for example. Indeed, you do not want to have your API keys displayed in your code, you will want to have them as environment variable.

To create a new secret :

To access you secret use :

process.env.mySuperAPIKey;
console.log(process.env.mySuperAPIKey);

Use the contact, company… properties in the Custom Coded Action

To access the properties, first go to the section Property to include in code. In that section, you can use any property of the current object, and you can choose the variable name you want.

In that example, we can access the contact’s email by using :

event.inputFields.email

Get the id of the current object

If you need the id of the current object enrolled in the WorkFlow you can use :

event.object.objectId

Run your code locally

Manage / access the secrets locally

To use a secret locally, add it to your .env file at the root the cli project.

Set your secret in the .env like so

privateAppToken = "vcf-no9-ggt09tnz-a522-4002-b012-jngpmsecx"
dropContactAPI= "qdkqsnksqnknvvvppmqmqsdds"

To access the secret in your code, in the cca.js where you write your code use :

process.env.privateAppToken

Watch the video to learn more :

Manage / access the objects’ properties locally

To set a property to include in code locally, add to the ./your-cca/event.js file the keys you need :

exports.events = {
    object: {
        objectId: 3401
    },
    inputFields: {
        domainName : "google.com",
        email : "hello@gmail.com"
    }
}

To access it in your code

    const domainName = event.inputFields.domainName;

    const email = event.inputFields.email;

    console.log('--> ', domainName, '-----> ', email)

Manage / access the current object’s id

If your code, uses the object’s ID of the current object in the workflow you can use.

event.object.objectId; // this returns the id it the current object

To set it up locally in the « event.js« ` update the value :

exports.events = {
    object: {
        objectId: 3401
    },
    inputFields: {
        domainName : "google.com",
        email : "hello@gmail.com"
    }
}

Test the code

To test the code type in your terminal :

npm run cca ./your-cca/cca.js

This runs your code and display what will be returned by the Custom Coded Action from the output function.

As you can see :

This very basic Custom Coded Action :

exports.main = async (event, callback) => {

    const domainName = event.inputFields.domainName;

    const email = event.inputFields.email;


    callback({
        outputFields: {
            domainName,
            email
        }
    });

}

Will display this in your terminal :

Deploy it

To deploy it nothing easier, copy the cca.js and paste in HubSpot.

Having unique data in a CRM system is important because it can help to avoid duplicate contacts, which can be annoying for customers and can also lead to lost opportunities. For example, if a customer is contacted by multiple salespeople, they may become frustrated and less likely to do business with you. Additionally, duplicate contacts can make it difficult to track customer interactions and to identify opportunities to upsell or cross-sell products or services.

What is needed to deduplicate ?

To deduplicate you need to have a unique key.
A unique key is a column or a combination of columns that uniquely identifies a record in a database. Unique keys are used to deduplicate data by identifying and removing duplicate records. This can improve the accuracy and reliability of data.

As an example when you register on an app like Instagram or Twitter they ask for your phone number. They use the phone number as a unique key if they need to deduplicate.
Indeed, it’s quite easy to have multiple email addresses, you can also use another name and nickname, but few people have more than one phone number. So for an APP like Instagram a phone number is a pretty good unique key.

In the example below we have two records, with somewhat similar names, they maybe are duplicates. But what unique key could we use here ? As you can see the only common key value pair in the two records is carPlate so we can use this key to deduplicate.

How to look for duplicates when a new data is inserted in HubSpot ?

To avoid duplicates, each time we get a new record in our CRM we can ask HubSpot to give us the list of all the records with a matching key value pair.

In our example, each time we get a new contact, we can ask HubSpot the list of contacts with the property carPlate = RM-123-IE

If he gets more than one record back, it means we have a duplicate.

In HubSpot you can use a workflow to run a logic each time something happens in our CRM. In that example each time we get a new contact with the carPlate value set.

The request looks like this :

As you can see on this schema, the request will return those two records :

Once we identified two records, we can compare them and decide the one we keep.
Deduplication works if we found 2 records, if we find more than 2 the logic is a bit more complicated as we have to perform two merges. It’s what we call an ambiguous merge. In this article, we will put this concept aside.

Merging criterias

Now that we have our duplicated records, we have to merge them. But before, we have to choose the record we keep and the one we remove.
That logic is up to you, but there are common logics.

Keep the record created first

With that criteria, the oldest recorded will remain, and the new one will be merged. It’s the most common criteria to merge.

Keep the record created last

As you can expect with that criteria, the latest record will remain and the oldest one will be merged into the newest.

Keep the record most recently updated

Keep the record which had the most recent update

Keep the record most recent engagement

Keep the record with the most recent meeting, calls, tasks…

Keep the record with the oldest engagement

Keep the record with the oldest meeting, calls, tasks…

How to set up a deduplication workflow in HubSpot ?

Create a workflow based on the type of objects you want to dedupe.
In this example, I create a contact based Workflow.

Add a trigger

A HubSpot workflow trigger is an event that causes a workflow to start. For example, you could create a workflow that is triggered when a contact fills out a form or in our case when criteria is met.
When the contact has a carPlate set, the workflow will start and the contact will be enrolled in the workflow.

Add a Custom Coded Action block

A custom coded action block in an HubSpot Workflow is a block of JavaScript or Python code that you can add to a workflow to perform custom actions. It’s a super block that you can use to create your own logic inside a workflow.

In our case we are going to use a Custom Coded Action block to add a logic to deduplicate our contacts based an the carPlate value.

Add your private app token

In the secret section you can save your API keys, pass phrases… that you don’t want to display in your custom code.

To deduplicate we need to call the HubSpot API so you need to have a private app token with the corresponding scope.

The scopes for our custom coded actions should be at least : read companies, write companies, read contacts, write contacts

As name you can use the name you want, in my setup I use the name privateAppToken

Paste the Custom Coded Action

First remove all the existing code then if you want to deduplicate contacts you have to use this code , if you want to deduplicate companies use that one.

Edit the Options

At the top of the code you can set the options you want.


/* * * Edit your Secret Name here */ const SECRET_NAME = "privateAppToken" /* * * Choose the dedupe key */ const DEDUPE_KEY = "carplate"; /* * Choose the merging criteria you want in that list * * Possible values : * * most-recent-engagement * oldest-engagement * created-first * created-last * most-recently-updated */ const MERGE_CRITERIA_KEEP_THE_ONE = "created-first"; /* * * If the DRY_RUN is true, no merge will be performed */ const DRY_RUN = false;

SECRET_NAME :

If your secret name is not privateAppToken edit the line :

const SECRET_NAME = "privateAppToken"

With your secret name like :

const SECRET_NAME = "myOwnPrivateAppName"

DEDUPE_KEY :

In my case If have the dedupe key carPlate but yours will be different so edit this line :

const DEDUPE_KEY = "carplate";

with your own key like :

const DEDUPE_KEY = "yourOwnKey";

To find the key you have to use you can follow this video :

MERGE_CRITERIA_KEEP_THE_ONE :

Set the merging criteria you want to use between those possibilities :

DRY_RUN :

If const DRY_RUN = false; then nothing will be merged, this option is interesting to test the logic.

Possible values :

const DRY_RUN = false;
const DRY_RUN = true;

Data outputs

In a Custom Coded Action, you can specify a return. In other words, what the Custom Coded Action will throw back to the Workflow. This Custom Coded Action returns couple of things

As an example :

To set the output underneath the code there’s a « Data outputs » section where you have to put the following :

It should look like this :

Turn the workflow on

TO turn the workflow on, you can follow this video :

NB : You can’t use the option Yes, enroll existing companies which meet the trigger criteria as of now as it will trigger API rate limit error. ( I’m working on it to find a fix)

Télécharger node depuis les sources ( apt-get…)

Franchement, c’est assez pénible et c’est prompt à encore plus d’erreur. Car si vous êtes ici, c’est que quelque chose ne se passe pas comme prévu.
Bref, je vous déconseille cette solution.

Utiliser le package « n »

Le plus simple et le plus efficace, parce que ce package va permettre d’activer une version de node en particulier.

Pour l’installer rien de plus simple :

npm install -g n   # Install n globally

Puis télécharger la version que l’on souhaite :

n <version>   # Install node <version>

Pour activer la version de node que l’on souhaite rien de plus simple, il suffit de juste taper :

n

Ensuite, il faut sélectionner la version que l’on souhaite.

En cas de problèmes :

Vérifiez quelle version de Node est active sur votre machine avec :

which node

Cela devrait retourner :

/usr/local/bin/node

Homebrew

Si vous avez installé Node avec Homebrew ce n’est pas impossible que l’installation de cette nouvelle version avec le package n n’active pas la version voulue.

Désinstaller node depuis Homebrew

brew uninstal node

Maintenant la seule version de Node activée devrait être celle qui vient de n

Et voilà, c’est terminé !

Enjoy ! 🙂

Le setup est extrêmement simple, il vous faudra l’accès à votre serveur Linux Debian ou Ubuntu. Il faudra aussi installer Apache2 si ce n’est pas déjà fait.

Déployez votre code sur le serveur

Je ne vais pas détailler ici la méthode pour téléverser votre code sur votre propre serveur, personnellement ma préférence va pour le déploiement via un repo Github, sur le serveur, j’effectue un git clone ou bien des git pull pour récupérer la dernière version du code.

Démarrez votre application

Ici vous avez le choix, pour faire au plus simple, personnellement, j’utilise le paquet Forever

forever start app.js

Créez un fichier de conf pour Apache

Comme nous volons déployer une application Node JS, l’application va être exécutée de manière locale sur le serveur, c’est-à-dire que l’application sera disponible sur localhost sur le serveur.

L’idée va t’être de demander à Apache de passer le trafic extérieur vers cette application locale. Pour cela, nous allons utiliser la fonction proxyPass d’ Apache.

Afin d’utiliser cette fonction, il faut bien évidemment qu’elle soit disponible dans votre configuration d’Apache2, pour ce faire, activez le mod suivant :

a2enmod proxy proxy_http

Une fois que ce mode est activé, créer un fichier de configuration.

Allez dans le dossier :

/etc/apache2/sites-available

Puis créez le fichier .conf suivant ce style de nomenclature

nomdusite.antoibrossault.com.conf

Le fichier se construit de manière suivante

<VirtualHost *:80>
    ServerAdmin contact@masterOfTheUniverse.com
    ServerName  my-app.antoinebrossault.com

    ProxyRequests off

    <Proxy *>
            Order deny,allow
            Allow from all
    </Proxy>

    <Location />
            ProxyPass http://localhost:8080/
            ProxyPassReverse http://localhost:8080/
    </Location>

RewriteEngine on
RewriteCond %{SERVER_NAME} =my-app.antoinebrossault.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Vous noterez que dans cet exemple notre application tourne un sur le port 8080.
Si votre application tourne sur un port différent bien évidemment, veuillez mettre à jour ces lignes.

Activez le site dans Apache2

a2ensite nomdusite.antoibrossault.com.conf

Lancez cert-bot de let’s encrypt pour obtenir un certicat SSL valide

Je ne vais pas détailler ici l’installation de cert-bot, mais c’est un outil extrêmement pratique pour récupérer, renouveler automatiquement les certificats de let’s encrypt.

certbot --apache

Vous devriez maintenant avoir votre application qui tourne avec un certificat SSL valide.

Conclusion

Cette approche n’est pas recommandée avec toutes sortes d’application, mais si vous êtes sur une application de test, expérimentale, c’est globalement une solution rapide et intéressante à avoir dans sa caisse à outils.

Dans un projet vue.js commencez par installer vueX

Installez vueX avec npm

npm install vuex --save

Créez un store

Créez un store dans votre application. J’ai décidé de créer un dossier store à la racine de mon projet.

Dans le fichier ./store/store.js


// ./store/store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); // Accesible with $store.state.count const state = { count: 3 }; export default new Vuex.Store({ state });

Demandez à vue.js d’utiliser le store

Dans main.js j’importe mon store à de manière globale puis je demande à vue.js de l’utiliser.


import Vue from 'vue' import App from './App' import store from './store/store' // j'importe mon store Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', store, // je demande de l'utiliser. template: '<App/>', components: { App } })

Les Getters

Nous pouvons créer des méthodes pour accéder à notre store. Pour ce faire nous créons nos méthodes :

const getters = {
   evenOrOdd : state => state.count % 2 === 0 ? 'even' : 'odd',
}

Maintenant nous avons une méthode pour interagir avec notre state. Mais comment y accéder depuis mon component ?

Il faut importer mapGetters dans notre component

Créer une méthode computed

NB Pour utiliser d’autres computed méthodes il faut utilise le spread operator.

computed :{

   ...mapGetters([
      'evenOrOdd'
    ]),
    hello: {
       get(){  return 42 },

    }
  }

Mon composant final avec les getters

<template>
  <div>
       Counter : {{$store.state.count}}, counter is {{ evenOrOdd }}
  </div>
</template>
<script>

import { mapGetters } from 'vuex'
import post from './post.vue';

export default {
  name: 'HelloWorld',
  components : {post},
  data(){
    return{
    }
  },
  computed :{

   ...mapGetters([
      'evenOrOdd'
    ]),
    hello: {
       get(){  return 42 },

    }
  }

}

</script>

Les setters

// ./store/store.js


const state = {
    count: 3
};


const mutations = {
     increment (state){
         state.count++
     },
     decrement (state){
         state.count--;
     }
}

const actions = {
    increment: (context) => context.commit('increment'),
    decrement: (context) => context.commit('decrement'),
    incrementIfOdd: (commit, state) => {
        if( (state.count + 1 ) % 2 === 0 ) commit('increment');
    },
    incrementAsync: ({commit}) => {
       return new Promise( (resolve, reject) => {
           setTimeout( () => {
                commit('increment');
                resolve();
           }, 1000); 
       })
    }
}

export default new Vuex.Store({
    state,
    getters,
    mutations,
    actions
});


Utilisez le store dans les composants


<template> <div class="hello"> Counter : {{$store.state.count}}, counter is {{ evenOrOdd }} <button @click="increment"> + </button> <button @click="decrement"> - </button> <button @click="incrementIfOdd"> + </button> <button @click="incrementAsync"> async </button> <post></post> <p> {{hello}} </p> </div> </template> <script> import { mapGetters, mapActions} from 'vuex' import post from './post.vue'; export default { name: 'HelloWorld', components : {post}, data(){ return{ } }, computed :{ ...mapGetters([ 'evenOrOdd' ]), hello: { get(){ return 42 }, } }, methods : mapActions([ 'increment', 'decrement', 'incrementIfOdd', 'incrementAsync' ]), } </script>

Installer les containers

Le plus simple est d’utiliser un fichier docker-compose.yml

Créez le fichier docker-compose.yml suivant à la racine de votre projet.

php_apache:
  image: webdevops/php-apache:7.1
  links:
    - db:mysql
  ports:
    - 8989:80
  volumes:
    - ./:/app/
    - ./php.ini/:/opt/docker/etc/php/php.ini
db:
  image: mariadb
  ports:
    - 3306:3306
  volumes:
    - "./data/:/var/lib/mysql"
  environment: 
    MYSQL_ROOT_PASSWORD: examplepass
phpmyadmin:
  image: corbinu/docker-phpmyadmin
  links:
    - db:mysql
  ports:
    - 8181:80
  environment:
    MYSQL_USERNAME: root
    MYSQL_ROOT_PASSWORD: examplepass

Démarrer les containers

Dans votre projet tapez docker-compose up -d à l’inverse docker-compose down

Pour vérifier les containers qui tournent actuellement docker ps

Utiliser les containers

Allez sur http://localhost:8989 pour accéder à votre projet.

configurer la bdd

Pour administrer la db depuis un db manager ( sequel pro.. )

host : 127.0.0.1
port : 3306
MYSQL_USERNAME: root
MYSQL_ROOT_PASSWORD: examplepass

NB : Pour connecter votre app à la db, utilisez : host : mysql

Tester la connection

Ici un simple test avec node.js

npm install mysql

var mysql      = require('mysql');


var connection = mysql.createConnection({
  host     : '127.0.0.1',
  user     : 'root',
  password : 'examplepass',
  database : 'information_schema'
});

connection.connect();

connection.query('SELECT * FROM ALL_PLUGINS', function (error, results, fields) {
  if (error) throw error;
  console.log('The solution is: ', results[0] );
});

connection.end();

Configurer PHP

Si cette configuration est destinée au dev, vous voulez surement afficher les erreurs PHP.

Pour ce faire, créez un fichier php.ini à la racine de votre projet.

Dans ce fichier ajoutez :

## PHP CONFIG
display_errors = On
display_startup_errors = On
error_reporting  =  E_ALL

Un point important à sous-lignée sur Angular c’est sur l’affichage des images. En effet si nous avions le chemin vers une image dans notre array de donnée nous serions tentés d’essayer de l’afficher comme ceci :

<img src="{{product.images[0].full}}"/>

Mais cela ne fonctionnera pas :/ ! Pourquoi me direz-vous ? En fait le navigateur va tout de suite essayer d’afficher l’image sans attendre qu’angular interprète l’expression et la remplace par le bon chemin.

Les petits gars de chez Google ont pensé à tout avec une directive prévue pour l’occasion, la directive ng-src pour afficher notre image nous devons donc faire.

<img ng-src="{{product.images[0].full}}"/>

Faites bien attention à utiliser des accolades pour qu’angular interprète bien la propriété

ng-src avec une boucle

Reprendre un petit exemple concret ne fait pas de mal alors nous allons voir comment faire une boucle pour afficher des images issues d’un sous array de notre controller.

(function() {
  var app = angular.module('gemStore', []);

  app.controller('StoreController', function() {
    this.products = gems;
  });

  var gems = [{
    name: 'Azurite',
    description: "Some gems have hidden qualities beyond their luster, beyond their shine... Azurite is one of those gems.",
    shine: 8,
    price: 110.50,
    rarity: 7,
    color: '#CCC',
    faces: 14,
    images: [
      "images/gem-02.gif",
      "images/gem-05.gif",
      "images/gem-09.gif"
    ]
  },{
    name: 'Zircon',
    description: "Zircon is our most coveted and sought after gem. You will pay much to be the proud owner of this gorgeous and high shine gem.",
    shine: 70,
    price: 1100,
    rarity: 2,
    color: '#000',
    faces: 6,
    images: [
      "images/gem-06.gif",
      "images/gem-07.gif",
      "images/gem-10.gif"
    ]
  }];
})();

<!DOCTYPE html>
<html ng-app="gemStore">
<head>
<link rel="stylesheet" type="text/css" href="bootstrap.min.css" />
<script type="text/JavaScript" src="angular.min.js"></script>
<script type="text/JavaScript" src="app.js"></script>
</head>
<body class="list-group" ng-controller="StoreController as store">
<!--  Product Container  -->
<div class="list-group-item" ng-repeat="product in store.products">
<h3>{{product.name}} <em class="pull-right">{{product.price | currency}}</em></h3>

<!-- Image Gallery  -->
<div class="gallery">
<div class="img-wrap">
<img ng-src="{{product.images[0]}}" />
</div>
<ul class="img-thumbnails clearfix">

<!-- ICI NOTRE BOUCLE -->

<li class="small-image pull-left thumbnail" ng-repeat="image in product.images">
<img ng-src="{{image}}" />
</li>

<!-- ICI NOTRE BOUCLE -->

</ul>
</div>
</div>
</body>
</html>


ng-click

Une autre directive tout aussi importante qu’essentielle, c’est ng-click qui permet assigner une valeur à une expression qu’Angular pourra interpréter. Vous comprendrez mieux ce concept avec l’exemple ci-dessous.

Imaginons que dans notre application de magasin en ligne nous voulions afficher des informations dans des tabs. Le principe d’une tab est que lorsque l’on clique sur le titre JavaScript cherche le contenu correspondant et l’affiche et cache les autres.

<section>
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>
{{tab}}
</section>

Que ce passe-t-il dans ce code ? En fait c’est très simple lorsque l’on clique sur Description / Specs / Reviews on assigne à l’expression tab une valeur, respectivement 1,2,3.
Vous remarquerez que dans notre exemple on affiche le contenu de l’expression {{tab}}, et quand un clique sur un élément avec la directive ng-click la valeur de tab change !

2-way- Data Binding

Mais c’est vraiment étrange que cette expression ce mette à jour, non ? C’est en fait un principe important sur Angular.js le 2-way- Data Binding. En fait à chaque fois que la propriété change Angular re-interprète l’expression.

conditionner dans une directive

Dans notre exemple précédent, vous avez dû remarquer que pour le moment nous n’affichions que l’expression après le click sur une tab. Mais maintenant nous voulons rendre la chose un peu plus concrète en affichant réellement les tables. Il faut donc que nous puissant dire à Angular :

« Quand je click sur une tab et que tab change de valeur, affiche la tab correspondante ».

Afficher / Cacher… Cela vous dit quelque chose ? Oui ! Il faut utiliser les directives ng-show !

ng-show permet de vérifier une condition il suffit de lui mettre cette condition en paramètre

<section>
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>

<div class="panel" ng-show="tab === 1">
<h1>1 </h1>
<p> {{product.description}} </p>
</div>

<div class="panel" ng-show="tab === 2">
<h1>2 </h1>
<p> {{product.description}} </p>
</div>

<div class="panel" ng-show="tab === 3">
<h1>3 </h1>
<p> {{product.description}} </p>
</div>

</section>

Ce qui nous donne, si tab à la valeur 2 alors affiche le contenu de cet élément html. Voilà ce que cela donne dans notre cas concret de pannel.

ng-init mettre en place une valeur par défaut à une expression

Dans notre exemple précédant, nous aimerions peut-être pouvoir afficher une tab au chargement de la page. Sans avoir à cliquer dessus. En suivant notre logique, il faudrait donc pouvoir assigner une valeur par défaut à tab… Angular propose une directive permettant de réaliser cela, c’est la directive ng-init

<section ng-init="tab = 1">
<ul class="nav nav-pills">
<li><a href ng-click="tab = 1"> Description</a></li>
<li><a href ng-click="tab = 2"> Specs</a></li>
<li><a href ng-click="tab = 3"> Reviews</a></li>
</ul>

...

</section>

Dans notre exemple ng-init nous permet d’assigner tab à une valeur de 1 dès le chargement de notre page.

ng-class ajouter / retirer une class en fonction d’une expression

Si vous avez l’habitude de faire de l’intégration , vous savez que nous avons souvent besoin d’ajouter des class en fonction de certaines conditions. C’est notamment le cas dans les pannels ou nous devons ajouter une class .active pour signifier le pannel courant. Angular nous permet de faire ce genre d’opération simplement avec ng-class.

Voilà comment elle fonctionne :

<li ng-class="{active:tab === 1}"></div>

En premier nous spécifions la class ici (active) en second l’expression à confronter avec notre condition.

<ul class="nav nav-pills">
<li ng-class="{active:tab === 1}">
<a href ng-click="tab = 1"> Description</a>
</li>
<li ng-class="{active:tab === 2}">
<a href ng-click="tab = 2"> Specs</a>
</li>
<li ng-class="{active:tab === 3}">
<a href ng-click="tab = 3"> Reviews</a>
</li>
</ul>

Pas de logique dans les vues…

Si vous regardez les exemple précédents, on commence à voir apparaitre de la logique dans nos vues (là où sont affichées les infos pour l’utilisateur). Et comme vous le savez, Angular.js est basé sur le modèle MVC (modèle / vue / controller).
Notre logique doit donc aller dans un controller pour éviter une bouillie de code incompréhensible.

Création d’un nouveau controller

La première étape est donc de créer une directive ng-controller sur notre élément :

<section ng-controller="PannelController as pannel">

  <ul class="nav nav-pills">
    <li ng-class="{active:pannel.isSelected(1)}">
      <a href ng-click="pannel.selectTab(1)"> Description</a>
    </li>
    <li ng-class="{active:pannel.isSelected(2)}">
      <a href ng-click="pannel.selectTab(2)"> Specs</a>
    </li>
    <li ng-class="{active:pannel.isSelected(3)}">
      <a href ng-click="pannel.selectTab(3)"> Reviews</a>
    </li>
  </ul>

  <div class="panel" ng-show="pannel.isSelected(1)">
    <h1>1 </h1>
    <p> {{product.description}} </p>
  </div>

  <div class="panel" ng-show="pannel.isSelected(2)">
    <h1>2 </h1>
    <p> {{product.description}} </p>
  </div>

  <div class="panel" ng-show="pannel.isSelected(3)">
    <h1>3 </h1>
    <p> {{product.description}} </p>
  </div>

</section>

la deuxième étape est de créer notre controller pour sa partie JavaScript.


app.controller("PannelController",function(){ this.tab = 1; this.selectTab = function(setTab){ this.tab = setTab; }; this.isSelected = function(checkTab){ if(this.tab === checkTab){ return true; } }) });

Explication, pour mettre notre class « .active » sur le bon élément on utilise la directive ng-class qui affichera la classe donnée en function d’une condition. Pour vérifier cette condition, on appelle dans notre controller la function (une function expression) de notre controller. Cette function vérifie si notre tab est actuellement sélectionné.

Par défaut c’est bien notre tab 1 qui est sélectionné, en étudiant notre controller on observe bien « this.tab = 1 » donc isSelected vérifie qu’elle est la valeur de tab.

selectTab est une function qui modifie la valeur de tab.