├── .eslintignore ├── src ├── client │ ├── modules │ │ ├── common │ │ │ ├── modal │ │ │ │ ├── modal.css │ │ │ │ ├── modal.js │ │ │ │ └── modal.html │ │ │ ├── notfound │ │ │ │ ├── notfound.css │ │ │ │ ├── notfound.js │ │ │ │ └── notfound.html │ │ │ ├── datapicker │ │ │ │ ├── datapicker.css │ │ │ │ └── datapicker.html │ │ │ ├── savemodal │ │ │ │ ├── savemodal.css │ │ │ │ ├── savemodal.js │ │ │ │ └── savemodal.html │ │ │ ├── welcomemat │ │ │ │ ├── welcomemat.css │ │ │ │ ├── welcomemat.js │ │ │ │ └── welcomemat.html │ │ │ ├── schemapicker │ │ │ │ ├── schemapicker.css │ │ │ │ └── schemapicker.html │ │ │ ├── welcomemattile │ │ │ │ ├── welcomemattile.css │ │ │ │ ├── welcomemattile.js │ │ │ │ └── welcomemattile.html │ │ │ ├── activity │ │ │ │ ├── testdata │ │ │ │ │ ├── requestCulture.json │ │ │ │ │ ├── requestExpressionBuilderAttributes.json │ │ │ │ │ ├── requestEntryEventDefinitionKey.json │ │ │ │ │ ├── requestEngineSettings.json │ │ │ │ │ ├── requestActivityPermissions.json │ │ │ │ │ ├── requestEndpoints.json │ │ │ │ │ ├── requestInteractionDefaults.json │ │ │ │ │ ├── requestTokens.json │ │ │ │ │ ├── requestSchema.json │ │ │ │ │ ├── requestTriggerEventDefinition.json │ │ │ │ │ ├── ready.json │ │ │ │ │ └── requestDataSources.json │ │ │ │ ├── activity.css │ │ │ │ ├── activity.html │ │ │ │ ├── testHarness.js │ │ │ │ └── activity.js │ │ │ ├── illustrationdesert │ │ │ │ ├── illustrationDesert.css │ │ │ │ ├── illustrationdesert.css │ │ │ │ ├── illustrationDesert.js │ │ │ │ └── illustrationdesert.js │ │ │ ├── alert │ │ │ │ ├── alert.css │ │ │ │ ├── alert.js │ │ │ │ └── alert.html │ │ │ ├── toast │ │ │ │ ├── toast.css │ │ │ │ ├── toast.html │ │ │ │ └── toast.js │ │ │ ├── splitview │ │ │ │ ├── splitview.css │ │ │ │ ├── splitview.js │ │ │ │ └── splitview.html │ │ │ └── utils │ │ │ │ └── utils.js │ │ ├── datatools │ │ │ ├── datatable │ │ │ │ ├── datatable.css │ │ │ │ ├── dataTable.js │ │ │ │ └── dataTable.html │ │ │ ├── savemodal │ │ │ │ ├── savemodal.css │ │ │ │ ├── savemodal.html │ │ │ │ └── savemodal.js │ │ │ ├── dataassessor │ │ │ │ ├── dataassessor.css │ │ │ │ └── dataassessor.html │ │ │ ├── filterpanel │ │ │ │ ├── filterpanel.css │ │ │ │ ├── filterPanel.html │ │ │ │ ├── filterpanel.html │ │ │ │ ├── filterPanel.js │ │ │ │ └── filterpanel.js │ │ │ ├── dataviewer │ │ │ │ ├── dataviewer.css │ │ │ │ └── dataviewer.html │ │ │ ├── fieldoptions │ │ │ │ ├── fieldoptions.css │ │ │ │ ├── fieldoptions.js │ │ │ │ └── fieldoptions.html │ │ │ └── app │ │ │ │ ├── app.css │ │ │ │ ├── app.js │ │ │ │ └── app.html │ │ ├── platformevent │ │ │ └── activity │ │ │ │ ├── activity.css │ │ │ │ └── activity.html │ │ ├── salesforcenotification │ │ │ └── activity │ │ │ │ ├── activity.css │ │ │ │ └── activity.html │ │ ├── jsconfig.json │ │ ├── lightningcustom │ │ │ ├── primitiveIcon │ │ │ │ ├── primitiveIcon.html │ │ │ │ ├── primitiveIcon.css │ │ │ │ └── fetch.js │ │ │ ├── iconUtils │ │ │ │ ├── iconUtils.js-meta.xml │ │ │ │ ├── isIframeInEdge.js │ │ │ │ ├── fetchSvg.js │ │ │ │ ├── supportsSvg.js │ │ │ │ ├── iconUtils.js │ │ │ │ └── polyfill.js │ │ │ ├── tree │ │ │ │ ├── tree.js-meta.xml │ │ │ │ ├── tree.html │ │ │ │ ├── treeNode.js │ │ │ │ └── treeDataGenerator.js │ │ │ ├── treeItem │ │ │ │ ├── treeItem.js-meta.xml │ │ │ │ └── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── treeItem.spec.js.snap │ │ │ │ │ └── treeItem.spec.js │ │ │ └── utilsPrivate │ │ │ │ ├── assert.js │ │ │ │ ├── classListMutation.js │ │ │ │ ├── scroll.js │ │ │ │ ├── guid.js │ │ │ │ ├── smartSetAttribute.js │ │ │ │ ├── browser.js │ │ │ │ ├── observers.js │ │ │ │ ├── videoUtils.js │ │ │ │ ├── phonify.js │ │ │ │ ├── normalize.js │ │ │ │ ├── keyboard.js │ │ │ │ ├── utility.js │ │ │ │ ├── eventEmitter.js │ │ │ │ ├── aria.js │ │ │ │ ├── linkify.js │ │ │ │ └── linkUtils.js │ │ └── salesforceconfig │ │ │ └── app │ │ │ ├── app.css │ │ │ ├── __tests__ │ │ │ └── app.test.js │ │ │ ├── app.js │ │ │ └── app.html │ ├── assets │ │ ├── lwc.png │ │ ├── favicon.ico │ │ ├── GitHub_Logo.png │ │ ├── notification.png │ │ ├── Linkedin_Logo.png │ │ ├── platformeventsicon.png │ │ ├── ACC_GT_Dimensional_Purple_RGB_pos.png │ │ └── ACC_GT_Dimensional_Purple_RGB_pos.svg │ ├── dataTools.js │ ├── salesforceconfig.js │ ├── platformeventactivity.js │ ├── salesforcenotification.js │ ├── index.js │ └── index.html └── server │ ├── sfmc │ ├── sfmc-api.js │ ├── content.js │ └── data.js │ ├── utils │ ├── logger.js │ └── jwt.js │ ├── resources │ └── ISO_639-1.json │ ├── salesforceconfig │ └── salesforceconfig-api.js │ ├── dataTools │ └── dataTools-api.js │ ├── api.js │ ├── platformevent │ └── platformevent-api.js │ ├── sfdc │ └── index.js │ └── salesforcenotification │ └── salesforcenotification-api.js ├── .prettierignore ├── .prettierrc ├── certificates └── buildCert.bat ├── .gitignore ├── test.js ├── example.env ├── .eslintrc.json ├── app.json ├── CONTRIBUTING.md ├── .github └── workflows │ └── codeql-analysis.yml ├── package.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /src/client/modules/common/modal/modal.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/notfound/notfound.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/datapicker/datapicker.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/savemodal/savemodal.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemat/welcomemat.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/datatools/datatable/datatable.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/datatools/savemodal/savemodal.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/schemapicker/schemapicker.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/datatools/dataassessor/dataassessor.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/datatools/filterpanel/filterpanel.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/platformevent/activity/activity.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemattile/welcomemattile.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/salesforcenotification/activity/activity.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestCulture.json: -------------------------------------------------------------------------------- 1 | { "culture": "en-NZ" } 2 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestExpressionBuilderAttributes.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /src/client/assets/lwc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/lwc.png -------------------------------------------------------------------------------- /src/client/modules/datatools/dataviewer/dataviewer.css: -------------------------------------------------------------------------------- 1 | .scrollinwindow { 2 | overflow-y: scroll; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/favicon.ico -------------------------------------------------------------------------------- /src/client/modules/common/illustrationdesert/illustrationDesert.css: -------------------------------------------------------------------------------- 1 | div { 2 | background-color: white; 3 | } 4 | -------------------------------------------------------------------------------- /src/client/modules/common/illustrationdesert/illustrationdesert.css: -------------------------------------------------------------------------------- 1 | div { 2 | background-color: white; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Default dist folder 2 | dist/ 3 | 4 | # Default resources folder 5 | src/client/assets 6 | src/client/slds -------------------------------------------------------------------------------- /src/client/assets/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/GitHub_Logo.png -------------------------------------------------------------------------------- /src/client/assets/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/notification.png -------------------------------------------------------------------------------- /src/client/assets/Linkedin_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/Linkedin_Logo.png -------------------------------------------------------------------------------- /src/client/modules/common/alert/alert.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | z-index: 999; 3 | position: fixed; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/client/modules/common/toast/toast.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | z-index: 999; 3 | position: fixed; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/client/assets/platformeventsicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/platformeventsicon.png -------------------------------------------------------------------------------- /src/client/modules/common/notfound/notfound.js: -------------------------------------------------------------------------------- 1 | import { LightningElement } from 'lwc'; 2 | 3 | export default class NotFound extends LightningElement {} 4 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemat/welcomemat.js: -------------------------------------------------------------------------------- 1 | import { LightningElement } from 'lwc'; 2 | 3 | export default class WelcomeMat extends LightningElement {} 4 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestEntryEventDefinitionKey.json: -------------------------------------------------------------------------------- 1 | { "entryEventDefinitionKey": "DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3" } 2 | -------------------------------------------------------------------------------- /src/client/assets/ACC_GT_Dimensional_Purple_RGB_pos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accenture/sfmc-customapp/HEAD/src/client/assets/ACC_GT_Dimensional_Purple_RGB_pos.png -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestEngineSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "journeyPauseEnabled": true, 3 | "queueVersion": 1, 4 | "preventPathOptimizerHoldback": false 5 | } 6 | -------------------------------------------------------------------------------- /src/client/modules/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | }, 5 | "typeAcquisition": { 6 | "include": ["jest"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestActivityPermissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "view": true, 3 | "edit": true, 4 | "failedViewPermissions": [], 5 | "failedEditPermissions": [] 6 | } 7 | -------------------------------------------------------------------------------- /src/client/modules/datatools/datatable/dataTable.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class DataTable extends LightningElement { 4 | @api fields; 5 | @api rows; 6 | } 7 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/primitiveIcon/primitiveIcon.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/iconUtils.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "overrides": [ 6 | { 7 | "files": "**/*.html", 8 | "options": { "parser": "lwc" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/client/modules/common/schemapicker/schemapicker.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/tree/tree.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 46.0 5 | -------------------------------------------------------------------------------- /src/client/modules/datatools/fieldoptions/fieldoptions.css: -------------------------------------------------------------------------------- 1 | .app { 2 | position: fixed; 3 | padding: 0; 4 | margin: 0; 5 | 6 | top: 0; 7 | left: 0; 8 | 9 | width: 100%; 10 | height: 100%; 11 | background: rgba(255, 255, 255, 0.5); 12 | } 13 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/treeItem/treeItem.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 46.0 5 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/assert.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | export function assert(condition, message) { 3 | if (process.env.NODE_ENV !== 'production') { 4 | if (!condition) { 5 | throw new Error(message); 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/client/modules/datatools/app/app.css: -------------------------------------------------------------------------------- 1 | .logoplaceholder { 2 | width: 20%; 3 | } 4 | .app { 5 | position: z; 6 | padding: 0; 7 | margin: 0; 8 | 9 | top: 0; 10 | left: 0; 11 | 12 | width: 100%; 13 | height: 100%; 14 | background: rgba(255, 255, 255, 0.5); 15 | } 16 | -------------------------------------------------------------------------------- /src/client/dataTools.js: -------------------------------------------------------------------------------- 1 | import '@lwc/synthetic-shadow'; 2 | import { createElement } from 'lwc'; 3 | import Home from 'datatools/app'; 4 | 5 | const app = createElement('custom-element', { is: Home }); 6 | // eslint-disable-next-line @lwc/lwc/no-document-query 7 | document.querySelector('#main').appendChild(app); 8 | -------------------------------------------------------------------------------- /src/client/salesforceconfig.js: -------------------------------------------------------------------------------- 1 | import '@lwc/synthetic-shadow'; 2 | import { createElement } from 'lwc'; 3 | import lwcapp from 'salesforceconfig/app'; 4 | const app = createElement('custom-element', { is: lwcapp }); 5 | // eslint-disable-next-line @lwc/lwc/no-document-query 6 | document.querySelector('#main').appendChild(app); 7 | -------------------------------------------------------------------------------- /src/client/platformeventactivity.js: -------------------------------------------------------------------------------- 1 | import '@lwc/synthetic-shadow'; 2 | import { createElement } from 'lwc'; 3 | import lwcapp from 'platformevent/activity'; 4 | 5 | const app = createElement('custom-element', { is: lwcapp }); 6 | // eslint-disable-next-line @lwc/lwc/no-document-query 7 | document.querySelector('#main').appendChild(app); 8 | -------------------------------------------------------------------------------- /src/client/salesforcenotification.js: -------------------------------------------------------------------------------- 1 | import '@lwc/synthetic-shadow'; 2 | import { createElement } from 'lwc'; 3 | import lwcapp from 'salesforcenotification/activity'; 4 | 5 | const app = createElement('custom-element', { is: lwcapp }); 6 | // eslint-disable-next-line @lwc/lwc/no-document-query 7 | document.querySelector('#main').appendChild(app); 8 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestEndpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "restHost": "rest.s7.exacttarget.com", 3 | "stackKey": "S7", 4 | "stackHost": "mc.s7.exacttarget.com", 5 | "ssoUrl": "https://mc.s7.exacttarget.com/cloud/tools/SSO.aspx?env=default&legacy=1&sk=S7", 6 | "fuelapiRestHost": "https://www-mc-s7.exacttargetapis.com/" 7 | } 8 | -------------------------------------------------------------------------------- /certificates/buildCert.bat: -------------------------------------------------------------------------------- 1 | ECHO Building certificates for HTTPS 2 | openssl genrsa -out private.key 4096 3 | openssl req -new -sha256 -out private.csr -key private.key -config openssl.conf 4 | openssl x509 -req -days 365 -in private.csr -signkey private.key -out private.crt -extensions v3_ca -extfile openssl.conf 5 | openssl x509 -in private.crt -out private.pem -outform PEM -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | //webpack requires index.js to build with HtmlWebpackPlugin 2 | import '@lwc/synthetic-shadow'; 3 | import { createElement } from 'lwc'; 4 | import NotFound from 'common/notfound'; 5 | 6 | const app = createElement('custom-element', { is: NotFound }); 7 | // eslint-disable-next-line @lwc/lwc/no-document-query 8 | document.querySelector('#main').appendChild(app); 9 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/activity.css: -------------------------------------------------------------------------------- 1 | /* footer { 2 | background: rgb(216, 237, 255); 3 | } */ 4 | .activitycontainer { 5 | background: white; 6 | } 7 | 8 | .activityfooter { 9 | height: 50px; 10 | } 11 | .activityheader { 12 | height: 45px; 13 | } 14 | .activitycontent { 15 | height: calc(100vh - 50px - 45px); 16 | overflow-y: auto; 17 | } 18 | -------------------------------------------------------------------------------- /src/client/modules/salesforceconfig/app/app.css: -------------------------------------------------------------------------------- 1 | /* footer { 2 | background: rgb(216, 237, 255); 3 | } */ 4 | .activitycontainer { 5 | height: 100vh; 6 | background: white; 7 | } 8 | 9 | .activityfooter { 10 | height: 8vh; 11 | } 12 | .activityheader { 13 | height: 5vh; 14 | } 15 | .activitycontent { 16 | height: 85vh; 17 | overflow-y: scroll; 18 | } 19 | -------------------------------------------------------------------------------- /src/client/modules/common/notfound/notfound.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/primitiveIcon/primitiveIcon.css: -------------------------------------------------------------------------------- 1 | /* 2 | "Temporary" fix for Edge SVG quirk. We can remove this when it is fixed either 3 | at the SLDS level or at the browser level. 4 | https://git.soma.salesforce.com/aura/lightning-global/issues/1349 5 | 6 | Also prevents IE11 from gacking during some interactions 7 | */ 8 | _:-ms-lang(x), svg { 9 | pointer-events: none; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/modules/common/datapicker/datapicker.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/classListMutation.js: -------------------------------------------------------------------------------- 1 | export function classListMutation(classList, config) { 2 | Object.keys(config).forEach((key) => { 3 | if (typeof key === 'string' && key.length) { 4 | if (config[key]) { 5 | classList.add(key); 6 | } else { 7 | classList.remove(key); 8 | } 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/scroll.js: -------------------------------------------------------------------------------- 1 | export function raf(fn) { 2 | let ticking = false; 3 | return function (event) { 4 | if (!ticking) { 5 | // eslint-disable-next-line @lwc/lwc/no-async-operation 6 | requestAnimationFrame(() => { 7 | fn.call(this, event); 8 | ticking = false; 9 | }); 10 | } 11 | ticking = true; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestInteractionDefaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": [ 3 | "{{Event.DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3.\"Email\"}}" 4 | ], 5 | "mobileNumber": [], 6 | "transactionKeys": null, 7 | "properties": { 8 | "analyticsTracking": { 9 | "enabled": true, 10 | "analyticsType": "google", 11 | "urlDomainsToTrack": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestTokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "0Wn8fhoj-mmrCC-BN0QELX57zY9162esGaQHolyJgkZniRRr-s7I4jWIJPlotzKUEPx2OEYagK0k3qeIOVxYiTVzbPL-YWMmZ_5xspbBXshelof9ayN94wTaR3O9mHTBY2DOAt3g8QhvCHk1WAMwYQiCWOioprFPAurAcjj_1F2mA40jrbrufpvEUjFO8W3wEAW0vcWCYmVVEugcdevN3hPDrXyE1w53pSANi5tmQqj0_zd95U7cdWufdu65Wut8K9B7aPGG2y-_GfgXleXNtjQ", 3 | "fuel2token": "7QV8UtIxjeKaYNQ9JvNRXN7K", 4 | "expires": 1607881541441, 5 | "stackKey": "S7" 6 | } 7 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/isIframeInEdge.js: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/jonathantneal/svg4everybody/pull/139 2 | // Remove this iframe-in-edge check once the following is resolved https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8323875/ 3 | const isEdgeUA = /\bEdge\/.(\d+)\b/.test(navigator.userAgent); 4 | const inIframe = window.top !== window.self; 5 | const isIframeInEdge = isEdgeUA && inIframe; 6 | 7 | export default isIframeInEdge; 8 | -------------------------------------------------------------------------------- /src/client/modules/common/illustrationdesert/illustrationDesert.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class Illustration extends LightningElement { 4 | @api type; 5 | privateSize = 'slds-illustration_small'; 6 | @api 7 | set size(value) { 8 | this.privateSize = 9 | value === 'large' 10 | ? 'slds-illustration slds-illustration_large' 11 | : 'slds-illustration slds-illustration_small'; 12 | } 13 | get size() { 14 | return this.privateSize; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/modules/common/illustrationdesert/illustrationdesert.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class Illustration extends LightningElement { 4 | @api type; 5 | privateSize = 'slds-illustration_small'; 6 | @api 7 | set size(value) { 8 | this.privateSize = 9 | value === 'large' 10 | ? 'slds-illustration slds-illustration_large' 11 | : 'slds-illustration slds-illustration_small'; 12 | } 13 | get size() { 14 | return this.privateSize; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/modules/common/savemodal/savemodal.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class Modal extends LightningElement { 4 | @api title; 5 | handleClose() { 6 | this.dispatchEvent( 7 | new CustomEvent('close', { 8 | bubbles: true, 9 | composed: true 10 | }) 11 | ); 12 | } 13 | handleSave() { 14 | this.dispatchEvent( 15 | new CustomEvent('save', { 16 | bubbles: true, 17 | composed: true 18 | }) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Log files 2 | logs 3 | *.log 4 | *-debug.log 5 | *-error.log 6 | 7 | # Standard lib folder 8 | /lib 9 | 10 | # Standard dist folder 11 | /dist 12 | 13 | # Tooling files 14 | node_modules 15 | 16 | # Temp directory 17 | /tmp 18 | 19 | # Jest coverage folder 20 | /coverage 21 | 22 | # MacOS system files 23 | .DS_Store 24 | 25 | # Windows system files 26 | Thumbs.db 27 | ehthumbs.db 28 | [Dd]esktop.ini 29 | $RECYCLE.BIN/ 30 | 31 | # Certificate Keys 32 | certificates/private.csr 33 | certificates/private.crt 34 | certificates/private.key 35 | certificates/private.pem 36 | 37 | # dev credentials 38 | .env -------------------------------------------------------------------------------- /src/client/modules/common/splitview/splitview.css: -------------------------------------------------------------------------------- 1 | .app { 2 | position: fixed; 3 | padding: 0; 4 | margin: 0; 5 | 6 | top: 0; 7 | left: 0; 8 | 9 | width: 100%; 10 | height: 100%; 11 | background: rgba(255, 255, 255, 0.5); 12 | } 13 | 14 | .toggle-button { 15 | height: 100%; 16 | width: 0.75rem; 17 | background: white; 18 | border-radius: 0; 19 | border: 1px solid #dddbda; 20 | } 21 | 22 | .panel { 23 | width: 33vh; 24 | } 25 | 26 | .mainmax { 27 | width: calc(100vw - 0.75rem); 28 | } 29 | .mainmin { 30 | width: calc(100vw - 33vh - 0.75rem); 31 | } 32 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemattile/welcomemattile.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class WelcomeMatTile extends LightningElement { 4 | @api title; 5 | @api name; 6 | @api description; 7 | @api iconName; 8 | 9 | handleClick(e) { 10 | e.stopPropagation(); 11 | e.preventDefault(); 12 | this.dispatchEvent( 13 | new CustomEvent('click', { 14 | bubbles: true, 15 | composed: true, 16 | detail: { name: this.name, action: 'selectApp' } 17 | }) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/modules/common/modal/modal.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export default class Modal extends LightningElement { 4 | @api title; 5 | @api canclose = false; 6 | handleClose() { 7 | this.dispatchEvent( 8 | new CustomEvent('close', { 9 | bubbles: true, 10 | composed: true 11 | }) 12 | ); 13 | } 14 | handleSave() { 15 | this.dispatchEvent( 16 | new CustomEvent('save', { 17 | bubbles: true, 18 | composed: true 19 | }) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const Redis = require('ioredis'); 3 | 4 | // const redisClient = new Redis( 5 | // 'redis://:p2c876e39fbd74d3f63a9e2c557ab2fdd0820bf3beb31d0ed687fd3f90367e5f7@ec2-100-25-126-248.compute-1.amazonaws.com:27999' 6 | // ); 7 | console.log(process.env.REDIS_URL); 8 | const redisClient = new Redis(process.env.REDIS_URL); 9 | 10 | async function run() { 11 | try { 12 | const res = await redisClient.set('foo', 'bar'); // returns promise which resolves to string, "OK" 13 | console.log(res); 14 | } catch (ex) { 15 | console.error('CATCH', ex); 16 | } 17 | } 18 | run(); 19 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SFMC LWC 6 | 11 | 12 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | SFMC_AUTHURL=https://mc7t1g5l24q50klr8c1gqkvj63d1.auth.marketingcloudapis.com/ 2 | SFMC_CLIENTID=kf9p94gkyplazy8lfllwgido 3 | SFMC_CLIENTSECRET=OTi6pOeQkttkaNDzDQVVhgpU 4 | SFMC_JWT=7fNgBrwLrBcAMT1yikyYZcQ5ASFWxh59DrDtZG00MAoK9chBcFS4_pE22GZf-7z00zu6shQ54m3JQnh3yoYUR04ryZhst7gIHNINTZ9_cWH6iIPz28PdwF0yrWodZlbJGWVChjPJHit0zRB3NrYn5Nx4lDJES70rXYZXbfkgVO5Vvc8JGF4XWcklUdvOMRcM3kB7gmHEmV16LuIIqwvvC7e2W7jWHkYfvuQlsxpvxZHwd9eHWR3PcjGAqb1HOw2 5 | SECRET_TOKEN=keyboardcat 6 | PORT=3002 7 | NODE_ENV=development 8 | LOG_LEVEL=silly 9 | REDIS_URL=redis://:p2c876e39fbd74d3f63a9e2c557ab2fdd0820bf3beb31d0ed687fd3f90367e5f7@ec2-100-25-126-248.compute-1.amazonaws.com:27999 -------------------------------------------------------------------------------- /src/client/modules/common/utils/utils.js: -------------------------------------------------------------------------------- 1 | export function getCookieByName(name) { 2 | try { 3 | return document.cookie 4 | .split(';') 5 | .find((row) => row.startsWith(name)) 6 | .split('=')[1]; 7 | } catch (ex) { 8 | this.dispatchEvent( 9 | new CustomEvent('error', { 10 | bubbles: true, 11 | composed: true, 12 | detail: { 13 | type: 'error', 14 | message: 'Issues with the session, try refreshing the page' 15 | } 16 | }) 17 | ); 18 | this.isLoading = false; 19 | throw ex; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "inclusive-language" 4 | ], 5 | "extends": [ 6 | "@salesforce/eslint-config-lwc/recommended" 7 | ], 8 | "rules": { 9 | "@lwc/lwc/no-async-operation": "warn", 10 | "@lwc/lwc/no-inner-html": "warn", 11 | "@lwc/lwc/no-document-query": "warn", 12 | "inclusive-language/use-inclusive-words": "error" 13 | }, 14 | "overrides": [ 15 | { 16 | "files": [ 17 | "src/server/**", 18 | "webpack.config.js", 19 | "lwc-services.config.js" 20 | ], 21 | "env": { 22 | "node": true 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /src/server/sfmc/sfmc-api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('../utils/logger'); 3 | const core = require('./core.js'); 4 | const csurf = require('csurf')(); 5 | 6 | const router = express.Router({ strict: true }); 7 | 8 | router.get('/auth/response', core.authenicate); 9 | 10 | // used to proxy requests directly to SFMC Rest API 11 | router.use('rest/*', csurf, core.checkAuth, async (req, res) => { 12 | try { 13 | logger.info('proxy router'); 14 | const proxyres = await core.restproxy(req); 15 | 16 | res.status(proxyres.status).json(proxyres.data); 17 | } catch (ex) { 18 | res.status(500).json(ex.message); 19 | } 20 | }); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/guid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function to generate an unique guid. 3 | * used on state objects to provide a performance aid when iterating 4 | * through the items and marking them for render 5 | * @returns {String} an unique string ID 6 | */ 7 | export function guid() { 8 | function s4() { 9 | return Math.floor((1 + Math.random()) * 0x10000) 10 | .toString(16) 11 | .substring(1); 12 | } 13 | 14 | return ( 15 | s4() + 16 | s4() + 17 | '-' + 18 | s4() + 19 | '-' + 20 | s4() + 21 | '-' + 22 | s4() + 23 | '-' + 24 | s4() + 25 | s4() + 26 | s4() 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/fetchSvg.js: -------------------------------------------------------------------------------- 1 | // Taken from https://git.soma.salesforce.com/aura/lightning-global/blob/999dc35f948246181510df6e56f45ad4955032c2/src/main/components/lightning/SVGLibrary/stamper.js#L38-L60 2 | export default function fetchSvg(url) { 3 | return new Promise((resolve, reject) => { 4 | const xhr = new XMLHttpRequest(); 5 | xhr.open('GET', url); 6 | xhr.send(); 7 | xhr.onreadystatechange = () => { 8 | if (xhr.readyState === 4) { 9 | if (xhr.status === 200) { 10 | resolve(xhr.responseText); 11 | } else { 12 | reject(xhr); 13 | } 14 | } 15 | }; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/server/utils/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | /* 3 | levels 4 | error: 0, 5 | warn: 1, 6 | info: 2, 7 | http: 3, 8 | verbose: 4, 9 | debug: 5, 10 | silly: 6 11 | */ 12 | 13 | const logger = winston.createLogger({ 14 | format: winston.format.json(), 15 | defaultMeta: { service: 'sfmc-lwcactivity' }, 16 | transports: [ 17 | new winston.transports.Console({ 18 | level: process.env.LOG_LEVEL || 'info', 19 | handleExceptions: true, 20 | format: winston.format.combine( 21 | winston.format.colorize(), 22 | winston.format.simple() 23 | ) 24 | }) 25 | ] 26 | }); 27 | 28 | module.exports = logger; 29 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/smartSetAttribute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set an attribute on an element, if it's a normal element 3 | * it will use setAttribute, if it's an LWC component 4 | * it will use the public property 5 | * 6 | * @param {HTMLElement} element The element to act on 7 | * @param {String} attribute the attribute to set 8 | * @param {Any} value the value to set 9 | */ 10 | export function smartSetAttribute(element, attribute, value) { 11 | if (element.tagName.match(/^LIGHTNING/i)) { 12 | attribute = attribute.replace(/-\w/g, (m) => m[1].toUpperCase()); 13 | element[attribute] = value ? value : null; 14 | } else if (value) { 15 | element.setAttribute(attribute, value); 16 | } else { 17 | element.removeAttribute(attribute); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/server/utils/jwt.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger'); 2 | const jwt = require('jsonwebtoken'); 3 | module.exports = { 4 | decode: (req, res, next) => { 5 | if (!req.body) { 6 | res.status(500).send('Body was empty'); 7 | } 8 | if (!process.env.SFMC_JWT) { 9 | res.status(500).send('JWT Signature not found to decode'); 10 | } 11 | try { 12 | req.body = jwt.verify( 13 | req.body.toString('utf8'), 14 | process.env.SFMC_JWT, 15 | { 16 | algorithm: 'HS256' 17 | } 18 | ); 19 | return next(); 20 | } catch (ex) { 21 | logger.error('req.body', req.body); 22 | res.status(401).send('JWT was not correctly signed'); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/client/modules/common/splitview/splitview.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | import { classSet } from 'lightning/utils'; 3 | export default class SplitView extends LightningElement { 4 | @track isPanelClosed = false; 5 | get computedMainClass() { 6 | let classes = classSet('panel slds-split-view_container'); 7 | if (this.isPanelClosed) { 8 | classes.add('mainmax'); 9 | } else { 10 | classes.add('mainmin'); 11 | } 12 | 13 | return classes.toString(); 14 | } 15 | 16 | togglePanel() { 17 | this.isPanelClosed = !this.isPanelClosed; 18 | } 19 | handleOpenPanel() { 20 | this.isPanelClosed = false; 21 | } 22 | handleClosePanel() { 23 | console.log('closing pannel'); 24 | this.isPanelClosed = true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/tree/tree.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /src/client/modules/datatools/savemodal/savemodal.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/supportsSvg.js: -------------------------------------------------------------------------------- 1 | import isIframeInEdge from './isIframeInEdge'; 2 | 3 | // Taken from https://git.soma.salesforce.com/aura/lightning-global/blob/999dc35f948246181510df6e56f45ad4955032c2/src/main/components/lightning/SVGLibrary/stamper.js#L89-L98 4 | // Which looks like it was inspired by https://github.com/jonathantneal/svg4everybody/blob/377d27208fcad3671ed466e9511556cb9c8b5bd8/lib/svg4everybody.js#L92-L107 5 | // Modify at your own risk! 6 | const newerIEUA = /\bTrident\/[567]\b|\bMSIE (?:9|10)\.0\b/; 7 | const webkitUA = /\bAppleWebKit\/(\d+)\b/; 8 | const olderEdgeUA = /\bEdge\/12\.(\d+)\b/; 9 | const isIE = 10 | newerIEUA.test(navigator.userAgent) || 11 | (navigator.userAgent.match(olderEdgeUA) || [])[1] < 10547 || 12 | (navigator.userAgent.match(webkitUA) || [])[1] < 537; 13 | 14 | const supportsSvg = !isIE && !isIframeInEdge; 15 | 16 | export default supportsSvg; 17 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemat/welcomemat.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": [ 3 | { 4 | "key": "Event.DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3.ContactKey", 5 | "type": "Text", 6 | "length": 50, 7 | "default": "secondValye", 8 | "isNullable": null, 9 | "isPrimaryKey": true 10 | }, 11 | { 12 | "key": "Event.DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3.Email", 13 | "type": "EmailAddress", 14 | "length": 254, 15 | "default": null, 16 | "isNullable": true, 17 | "isPrimaryKey": null 18 | }, 19 | { 20 | "key": "Event.DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3.restrictedvalue", 21 | "type": "Text", 22 | "length": 50, 23 | "default": "happy", 24 | "isNullable": true, 25 | "isPrimaryKey": null 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/client/modules/common/alert/alert.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class Alert extends LightningElement { 4 | visible = false; 5 | @track privateAlert = {}; 6 | @api 7 | get alertPayload() { 8 | return this.privateAlert; 9 | } 10 | 11 | set alertPayload(alertPayload) { 12 | console.log('alert message', JSON.stringify(alertPayload)); 13 | if ( 14 | alertPayload && 15 | alertPayload.type && 16 | alertPayload !== this.privateAlert 17 | ) { 18 | this.privateAlert = alertPayload; 19 | this.visible = true; 20 | } 21 | } 22 | 23 | handleDismiss() { 24 | console.log('dismissed'); 25 | this.visible = false; 26 | this.privateAlert = {}; 27 | } 28 | 29 | handleShow() { 30 | this.visible = true; 31 | } 32 | logSomething(e) { 33 | console.log('logsomething', e); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/browser.js: -------------------------------------------------------------------------------- 1 | export const isIE11 = isIE11Test(navigator); 2 | export const isChrome = isChromeTest(navigator); 3 | export const isSafari = isSafariTest(navigator); 4 | 5 | // The following functions are for tests only 6 | export function isIE11Test(navigator) { 7 | // https://stackoverflow.com/questions/17447373/how-can-i-target-only-internet-explorer-11-with-javascript 8 | return /Trident.*rv[ :]*11\./.test(navigator.userAgent); 9 | } 10 | 11 | export function isChromeTest(navigator) { 12 | // https://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome 13 | return ( 14 | /Chrome/.test(navigator.userAgent) && 15 | /Google Inc/.test(navigator.vendor) 16 | ); 17 | } 18 | 19 | export function isSafariTest(navigator) { 20 | // via https://stackoverflow.com/questions/49872111/detect-safari-and-stop-script 21 | return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 22 | } 23 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/observers.js: -------------------------------------------------------------------------------- 1 | // hide panel on scroll 2 | const POSITION_CHANGE_THRESHOLD = 5; 3 | export function observePosition( 4 | target, 5 | threshold = POSITION_CHANGE_THRESHOLD, 6 | originalRect, 7 | callback 8 | ) { 9 | // retrieve current bounding client rect of target element 10 | const newBoundingRect = target.getBoundingClientRect(); 11 | const newLeft = newBoundingRect.left; 12 | const newTop = newBoundingRect.top; 13 | 14 | // old bounding rect values 15 | const oldLeft = originalRect.left; 16 | const oldTop = originalRect.top; 17 | 18 | // if we have a position change (horizontal or vertical) equal or greater to the threshold then execute the callback 19 | const horizontalShiftDelta = Math.abs(newLeft - oldLeft); 20 | const verticalShiftDelta = Math.abs(newTop - oldTop); 21 | 22 | if (horizontalShiftDelta >= threshold || verticalShiftDelta >= threshold) { 23 | callback(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/client/modules/common/toast/toast.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/modules/datatools/app/app.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | 3 | export default class App extends LightningElement { 4 | @track currentapp; 5 | @track alert = {}; 6 | 7 | get isdataassessor() { 8 | return this.currentapp === 'dataassessor'; 9 | } 10 | get isdataviewer() { 11 | return this.currentapp === 'dataviewer'; 12 | } 13 | 14 | get isNone() { 15 | return this.currentapp == null; 16 | } 17 | 18 | showAlert(e) { 19 | if (e.detail.type) { 20 | this.alert = e.detail; 21 | } else { 22 | this.alert = { 23 | type: 'error', 24 | message: e.detail.message || JSON.stringify(e.detail.errors) 25 | }; 26 | } 27 | } 28 | 29 | selectApp(e) { 30 | if (e.detail && e.detail.name) { 31 | this.currentapp = e.detail.name; 32 | } else { 33 | this.currentapp = null; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/client/assets/ACC_GT_Dimensional_Purple_RGB_pos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/client/modules/common/alert/alert.html: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /src/client/modules/common/welcomemattile/welcomemattile.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/videoUtils.js: -------------------------------------------------------------------------------- 1 | const ALLOWED_DOMAINS = new Set([ 2 | 'www.youtube.com', 3 | 'player.vimeo.com', 4 | 'play.vidyard.com', 5 | 'players.brightcove.net', 6 | 'bcove.video', 7 | 'player.cloudinary.com', 8 | 'fast.wistia.net', 9 | 'i1.adis.ws', 10 | 's1.adis.ws', 11 | 'scormanywhere.secure.force.com', 12 | 'appiniummastertrial.secure.force.com' 13 | ]); 14 | 15 | export function hasOnlyAllowedVideoIframes(htmlString) { 16 | if (htmlString && htmlString.indexOf(' -1) { 17 | const parsedHtml = new DOMParser().parseFromString( 18 | htmlString, 19 | 'text/html' 20 | ); 21 | const iframesList = Array.prototype.slice.call( 22 | parsedHtml.querySelectorAll('iframe') 23 | ); 24 | 25 | return ( 26 | iframesList.length > 0 && 27 | !iframesList.some((iframe) => !isUrlAllowed(iframe.src)) 28 | ); 29 | } 30 | return false; 31 | } 32 | 33 | function isUrlAllowed(url) { 34 | const anchor = document.createElement('a'); 35 | anchor.href = url; 36 | 37 | return anchor.protocol === 'https:' && ALLOWED_DOMAINS.has(anchor.hostname); 38 | } 39 | -------------------------------------------------------------------------------- /src/client/modules/common/toast/toast.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api } from 'lwc'; 2 | 3 | export function showToastEvent(thisPrivate, e, timeout) { 4 | thisPrivate.template 5 | .querySelector('common-toast') 6 | .showToastEvent(e, timeout); 7 | } 8 | export default class Toast extends LightningElement { 9 | title; 10 | message; 11 | variant; 12 | mode; 13 | isVisible; 14 | timer; 15 | 16 | @api 17 | showToastEvent(event, timeout) { 18 | this.isVisible = true; 19 | const timer = setTimeout( 20 | () => { 21 | this.clearToast(); 22 | clearTimeout(timer); 23 | }, 24 | timeout > 0 ? timeout : 3000 25 | ); 26 | this.title = event.title; 27 | this.message = event.message; 28 | this.variant = event.variant || 'success'; 29 | } 30 | 31 | clearToast() { 32 | this.isVisible = false; 33 | this.title = null; 34 | this.message = null; 35 | this.variant = null; 36 | } 37 | get icon() { 38 | return 'utility:' + this.variant; 39 | } 40 | get variantClass() { 41 | return 'slds-notify slds-notify_toast slds-theme_' + this.variant; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/phonify.js: -------------------------------------------------------------------------------- 1 | const locale = 'en-US'; 2 | const NA_PHONE_NUMBER = '($1) $2-$3'; 3 | const IS_TEN_DIGITS = /^\d{10}$/; 4 | const TEN_TO_NA = /(\d{3})(\d{3})(\d{4})/; 5 | const IS_ELEVEN_DIGITS = /^1\d{10}/; 6 | const ELEVEN_TO_NA = /1(\d{3})(\d{3})(\d{4})$/; 7 | 8 | // The locale argument has been added for tests since there's currently no clean way of mocking the locale 9 | export function toNorthAmericanPhoneNumber(value, userLocale) { 10 | if (!isNorthAmericanCountry(userLocale || locale)) { 11 | return value; 12 | } 13 | if (IS_TEN_DIGITS.test(value)) { 14 | return value.replace(TEN_TO_NA, NA_PHONE_NUMBER); 15 | } else if (IS_ELEVEN_DIGITS.test(value)) { 16 | return value.replace(ELEVEN_TO_NA, NA_PHONE_NUMBER); 17 | } 18 | return value || ''; 19 | } 20 | 21 | function isNorthAmericanCountry(userLocale) { 22 | const localeCountry = getLocaleCountry(userLocale); 23 | if (localeCountry === 'US' || localeCountry === 'CA') { 24 | return true; 25 | } 26 | return false; 27 | } 28 | 29 | function getLocaleCountry(userLocale) { 30 | if (!userLocale) { 31 | // just adding a guard in case locale is undefined 32 | return null; 33 | } 34 | const [, country] = userLocale.split('-'); 35 | return country; 36 | } 37 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/tree/treeNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: MIT 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT 6 | */ 7 | 8 | function computeKey(parentKey, childNum) { 9 | if (!parentKey) { 10 | return '0'; 11 | } 12 | if (parentKey === '0') { 13 | return `${childNum}`; 14 | } 15 | return `${parentKey}.${childNum}`; 16 | } 17 | 18 | export function getTreeNode(node, level, parentKey, childNum) { 19 | return { 20 | name: node.name, 21 | label: node.label, 22 | metatext: node.metatext, 23 | level, 24 | key: computeKey(parentKey, childNum), 25 | // eslint-disable-next-line no-script-url 26 | href: node.href || 'javascript:void(0)', 27 | isDisabled: node.disabled || false, 28 | visible: level === 1, 29 | children: [], 30 | visibleItems: [], 31 | nodeRef: node, 32 | isLeaf: 33 | !node.items || 34 | (Array.isArray(node.items) && node.items.length === 0), 35 | get isExpanded() { 36 | return this.isLeaf ? true : node.expanded || false; 37 | }, 38 | focusedChild: null, 39 | get strexpanded() { 40 | return (this.isLeaf 41 | ? true 42 | : this.nodeRef.expanded || false 43 | ).toString(); 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/client/modules/common/modal/modal.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SFMC-LWCActivity", 3 | "description": "An example custom app/activity for herokyy", 4 | "repository": "https://github.com/Accenture/sfmc-customapp", 5 | "logo": "https://www.accenture.com/_acnmedia/Thought-Leadership-Assets/Images/mainpage/Accenture-acn-mobile-logo-2", 6 | "keywords": [ 7 | "node", 8 | "express", 9 | "static", 10 | "sfmc", 11 | "marketingcloud", 12 | "lwc", 13 | "lightning" 14 | ], 15 | "env": { 16 | "SFMC_AUTHURL": { 17 | "description": "Auth URL from SFMC Installed Package", 18 | "required": true 19 | }, 20 | "SFMC_CLIENTID": { 21 | "description": "ClientId from SFMC Installed Package", 22 | "required": true 23 | }, 24 | "SFMC_CLIENTSECRET": { 25 | "description": "ClientSecret from SFMC Installed Package", 26 | "required": true 27 | }, 28 | "SECRET_TOKEN": { 29 | "description": "A secret key for verifying the integrity of signed cookies.", 30 | "generator": "secret" 31 | }, 32 | "SFMC_JWT": { 33 | "description": "JWT signature from SFMC Installed Package", 34 | "required": true 35 | } 36 | }, 37 | "addons": [ 38 | { 39 | "plan": "heroku-redis:hobby-dev" 40 | } 41 | ], 42 | "buildpacks": [ 43 | { 44 | "url": "https://github.com/heroku/heroku-buildpack-nodejs" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

Contributing to sfpowerkit

2 |

First and foremost, thank you! We appreciate that you want to contribute to sfpowerkit, your time is valuable, and your contributions mean a lot to us.

3 |

Important!

4 |

By contributing to this project, you:

5 | 12 |

Getting started

13 |

What does "contributing" mean?

14 |

Creating an issue is the simplest form of contributing to a project. But there are many ways to contribute, including the following:

15 | 20 |

Issues

21 |

Please only create issues for bug reports or feature requests. Issues discussing any other topics may be closed by the project's maintainers without further explanation.

22 |

Do not create issues about bumping dependencies unless a bug has been identified and you can demonstrate that it effects this repo.

23 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestTriggerEventDefinition.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventDefinition": { 3 | "id": "FED6E706-2B63-4FA8-82A2-87E850725262", 4 | "name": "lWC", 5 | "mode": "Production", 6 | "isVisibleInPicker": false, 7 | "type": "EmailAudience", 8 | "iconUrl": "/images/icon-data-extension.svg", 9 | "eventDefinitionKey": "DEAudience-571f35a8-ca78-ee41-4b24-37de63e9b12d", 10 | "dataExtensionId": "b41b04f3-09f6-ea11-a2eb-1402ec937095", 11 | "createdDate": "2020-09-13T14:00:06.880Z", 12 | "createdBy": 7545830, 13 | "modifiedDate": "2020-09-13T14:00:06.880Z", 14 | "modifiedBy": 7545830, 15 | "sourceApplicationExtensionId": "97e942ee-6914-4d3d-9e52-37ecb71f79ed", 16 | "isPlatformObject": false, 17 | "category": "Audience", 18 | "metaData": { 19 | "scheduleState": "No Schedule", 20 | "criteriaDescription": "", 21 | "scheduleFlowMode": "runOnce", 22 | "runOnceScheduleMode": "onPublish" 23 | }, 24 | "arguments": { 25 | "serializedObjectType": 3, 26 | "eventDefinitionId": "fed6e706-2b63-4fa8-82a2-87e850725262", 27 | "eventDefinitionKey": "DEAudience-571f35a8-ca78-ee41-4b24-37de63e9b12d", 28 | "dataExtensionId": "b41b04f3-09f6-ea11-a2eb-1402ec937095", 29 | "criteria": "", 30 | "useHighWatermark": false, 31 | "automationId": "4c97412b-3469-4e93-9940-7d81b86cf633" 32 | }, 33 | "configurationArguments": { "unconfigured": false } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/modules/datatools/datatable/dataTable.html: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /src/client/modules/datatools/filterpanel/filterPanel.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /src/client/modules/datatools/filterpanel/filterpanel.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/treeItem/__tests__/__snapshots__/treeItem.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`c-tree-item renders leaf tree-item 1`] = ` 4 | 5 | #shadow-root(open) 6 |
9 | 35 | 38 | 43 | 47 | label 48 | 49 | 53 | 56 | : 57 | 58 | metatext 59 | 60 | 61 | 62 |
63 |
64 | `; 65 | -------------------------------------------------------------------------------- /src/client/modules/salesforceconfig/app/__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | // These tests are examples to get you started on how how to test 2 | // Lightning Web Components using the Jest testing framework. 3 | // 4 | // See the LWC Recipes Open Source sample application for many other 5 | // test scenarios and best practices. 6 | // 7 | // https://github.com/trailheadapps/lwc-recipes-oss 8 | 9 | import { createElement } from 'lwc'; 10 | import MyApp from 'my/app'; 11 | 12 | describe('my-app', () => { 13 | afterEach(() => { 14 | // The jsdom instance is shared across test cases in a single file so reset the DOM 15 | while (document.body.firstChild) { 16 | document.body.removeChild(document.body.firstChild); 17 | } 18 | }); 19 | 20 | it('contains a link to the LWC documentation with target set to _blank', () => { 21 | const element = createElement('my-app', { 22 | is: MyApp 23 | }); 24 | document.body.appendChild(element); 25 | 26 | // Get link 27 | const linkEl = element.shadowRoot.querySelector('a'); 28 | 29 | expect(linkEl.target).toBe('_blank'); 30 | }); 31 | 32 | it('contains a link to the LWC documentation with https://', () => { 33 | const element = createElement('my-app', { 34 | is: MyApp 35 | }); 36 | document.body.appendChild(element); 37 | 38 | // Get link 39 | const linkEl = element.shadowRoot.querySelector('a'); 40 | 41 | expect(linkEl.href).toMatch(/^https:/); 42 | }); 43 | 44 | it('contains one active custom element my-greeting', () => { 45 | const element = createElement('my-app', { 46 | is: MyApp 47 | }); 48 | document.body.appendChild(element); 49 | 50 | // Get array of my-greeting custom elements 51 | const greetingEls = element.shadowRoot.querySelectorAll('my-greeting'); 52 | 53 | expect(greetingEls.length).toBe(1); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | A string normalization utility for attributes. 3 | @param {String} value - The value to normalize. 4 | @param {Object} config - The optional configuration object. 5 | @param {String} [config.fallbackValue] - The optional fallback value to use if the given value is not provided or invalid. Defaults to an empty string. 6 | @param {Array} [config.validValues] - An optional array of valid values. Assumes all input is valid if not provided. 7 | @return {String} - The normalized value. 8 | **/ 9 | export function normalizeString(value, config = {}) { 10 | const { fallbackValue = '', validValues, toLowerCase = true } = config; 11 | let normalized = (typeof value === 'string' && value.trim()) || ''; 12 | normalized = toLowerCase ? normalized.toLowerCase() : normalized; 13 | if (validValues && validValues.indexOf(normalized) === -1) { 14 | normalized = fallbackValue; 15 | } 16 | return normalized; 17 | } 18 | 19 | /** 20 | A boolean normalization utility for attributes. 21 | @param {Any} value - The value to normalize. 22 | @return {Boolean} - The normalized value. 23 | **/ 24 | export function normalizeBoolean(value) { 25 | return typeof value === 'string' || !!value; 26 | } 27 | 28 | export function normalizeArray(value) { 29 | if (Array.isArray(value)) { 30 | return value; 31 | } 32 | return []; 33 | } 34 | 35 | /** 36 | A aria attribute normalization utility. 37 | @param {Any} value - A single aria value or an array of aria values 38 | @return {String} - A space separated list of aria values 39 | **/ 40 | export function normalizeAriaAttribute(value) { 41 | let arias = Array.isArray(value) ? value : [value]; 42 | arias = arias 43 | .map((ariaValue) => { 44 | if (typeof ariaValue === 'string') { 45 | return ariaValue.replace(/\s+/g, ' ').trim(); 46 | } 47 | return ''; 48 | }) 49 | .filter((ariaValue) => !!ariaValue); 50 | 51 | return arias.length > 0 ? arias.join(' ') : null; 52 | } 53 | -------------------------------------------------------------------------------- /src/client/modules/datatools/filterpanel/filterPanel.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class FilterPanel extends LightningElement { 4 | @api fields; 5 | @track filter = {}; 6 | 7 | operators = [ 8 | { value: 'equals', label: '=' }, 9 | { value: 'notEquals', label: '!=' }, 10 | { value: 'greaterThan', label: '>' }, 11 | { value: 'lessThan', label: '<' }, 12 | { value: 'isNull', label: 'IS NULL' }, 13 | { value: 'isNotNull', label: 'IS NOT NULL' }, 14 | { value: 'greaterThanOrEqual', label: '>=' }, 15 | { value: 'lessThanOrEqual', label: '<=' }, 16 | { value: 'between', label: 'BETWEEN' }, 17 | { value: 'IN', label: 'IN' }, 18 | { value: 'like', label: 'LIKE' } 19 | ]; 20 | 21 | get privateFields() { 22 | return this.fields.map((f) => { 23 | const a = { ...f }; 24 | a.value = f.fieldName; 25 | a.label = f.label; 26 | return a; 27 | }); 28 | } 29 | 30 | operatorHandler(e) { 31 | if (e.detail.value) { 32 | this.filter.operator = e.detail.value; 33 | } else { 34 | delete this.filter.operator; 35 | } 36 | this.fireEvent(this.filter); 37 | } 38 | fieldHandler(e) { 39 | if (e.detail.value) { 40 | this.filter.field = e.detail.value; 41 | } else { 42 | delete this.filter.field; 43 | } 44 | this.fireEvent(this.filter); 45 | } 46 | valueHandler(e) { 47 | if (e.target.value) { 48 | this.filter.value = e.target.value; 49 | } else { 50 | delete this.filter.value; 51 | } 52 | this.fireEvent(this.filter); 53 | } 54 | fireEvent(filter) { 55 | console.log(JSON.stringify(filter)); 56 | this.dispatchEvent( 57 | new CustomEvent('filterchange', { 58 | bubbles: true, 59 | composed: true, 60 | detail: filter 61 | }) 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/client/modules/datatools/filterpanel/filterpanel.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | 3 | export default class FilterPanel extends LightningElement { 4 | @api fields; 5 | @track filter = {}; 6 | 7 | operators = [ 8 | { value: 'equals', label: '=' }, 9 | { value: 'notEquals', label: '!=' }, 10 | { value: 'greaterThan', label: '>' }, 11 | { value: 'lessThan', label: '<' }, 12 | { value: 'isNull', label: 'IS NULL' }, 13 | { value: 'isNotNull', label: 'IS NOT NULL' }, 14 | { value: 'greaterThanOrEqual', label: '>=' }, 15 | { value: 'lessThanOrEqual', label: '<=' }, 16 | { value: 'between', label: 'BETWEEN' }, 17 | { value: 'IN', label: 'IN' }, 18 | { value: 'like', label: 'LIKE' } 19 | ]; 20 | 21 | get privateFields() { 22 | return this.fields.map((f) => { 23 | const a = { ...f }; 24 | a.value = f.fieldName; 25 | a.label = f.label; 26 | return a; 27 | }); 28 | } 29 | 30 | operatorHandler(e) { 31 | if (e.detail.value) { 32 | this.filter.operator = e.detail.value; 33 | } else { 34 | delete this.filter.operator; 35 | } 36 | this.fireEvent(this.filter); 37 | } 38 | fieldHandler(e) { 39 | if (e.detail.value) { 40 | this.filter.field = e.detail.value; 41 | } else { 42 | delete this.filter.field; 43 | } 44 | this.fireEvent(this.filter); 45 | } 46 | valueHandler(e) { 47 | if (e.target.value) { 48 | this.filter.value = e.target.value; 49 | } else { 50 | delete this.filter.value; 51 | } 52 | this.fireEvent(this.filter); 53 | } 54 | fireEvent(filter) { 55 | console.log(JSON.stringify(filter)); 56 | this.dispatchEvent( 57 | new CustomEvent('filterchange', { 58 | bubbles: true, 59 | composed: true, 60 | detail: filter 61 | }) 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/activity.html: -------------------------------------------------------------------------------- 1 | 54 | -------------------------------------------------------------------------------- /src/server/sfmc/content.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | /** @description saves content to content builder 3 | * @memberof server/sfmc 4 | * @function 5 | * @param {Object} auth object from Marketing Cloud session auth token from session 6 | * @param {Object} payload for creating Asset 7 | * @return {Response} 8 | */ 9 | exports.postToContentBuilder = async (auth, payload) => { 10 | const restURL = `${auth.rest_instance_url}asset/content/assets/${ 11 | payload.id ? payload.id : '' 12 | }`; 13 | const rawRes = await fetch(restURL, { 14 | method: payload.id ? 'put' : 'post', 15 | body: JSON.stringify(payload), 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | Authorization: 'Bearer ' + auth.access_token 19 | } 20 | }); 21 | const parsedRed = await rawRes.json(); 22 | logger.info(parsedRed); 23 | if (rawRes.status < 300) { 24 | return parsedRed; 25 | } 26 | throw new Error(parsedRed); 27 | }; 28 | /** @description checks if file already exists with this name 29 | * @memberof server/sfmc 30 | * @function 31 | * @param {Object} auth object from Marketing Cloud session auth token from session 32 | * @param {Object} payload for creating Asset 33 | * @return {Response} 34 | */ 35 | exports.checkFileName = async (auth, name) => { 36 | const restURL = `${auth.rest_instance_url}asset/content/assets/query`; 37 | const body = { 38 | query: { 39 | property: 'name', 40 | simpleOperator: 'equal', 41 | value: name 42 | } 43 | }; 44 | const rawRes = await fetch(restURL, { 45 | method: 'post', 46 | body: JSON.stringify(body), 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | Authorization: 'Bearer ' + auth.access_token 50 | } 51 | }); 52 | const parsedRed = await rawRes.json(); 53 | if (rawRes.status === 401) { 54 | throw new Error(parsedRed); 55 | } else if (rawRes.status < 300) { 56 | return parsedRed; 57 | } else { 58 | throw new Error(parsedRed); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/client/modules/common/splitview/splitview.html: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /src/client/modules/common/savemodal/savemodal.html: -------------------------------------------------------------------------------- 1 | 58 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testHarness.js: -------------------------------------------------------------------------------- 1 | // this function is for example purposes only. it sets ups a Postmonger 2 | // session that emulates how Journey Builder works. You can call jb.ready() 3 | // from the console to kick off the initActivity event with a mock activity object 4 | export default function setupTestHarness(jbSession) { 5 | const isLocalhost = 6 | window.location.hostname === 'localhost' || 7 | window.location.hostname === '127.0.0.1'; 8 | if (!isLocalhost) { 9 | console.log('[setupTestHarness]', 'loading prod data'); 10 | // don't load the test harness functions when running in Journey Builder 11 | 12 | return; 13 | } 14 | console.log('[setupTestHarness]', 'loading test data'); 15 | const jb = {}; 16 | window.jb = jb; 17 | 18 | //standard responses 19 | 20 | const events = [ 21 | 'requestContactsSchema', 22 | 'requestSchema', 23 | 'requestTriggerEventDefinition', 24 | 'requestDataSources', 25 | 'requestTokens' 26 | ]; 27 | 28 | for (const e of events) { 29 | try { 30 | jbSession.on(e, () => { 31 | console.log('[echo]', e); 32 | jbSession.trigger( 33 | e.replace('request', 'requested'), 34 | require(`./testdata/${e}.json`) 35 | ); 36 | }); 37 | } catch (ex) { 38 | console.error('Could not load test harness for event ', e, ex); 39 | } 40 | } 41 | // custom responses 42 | 43 | jbSession.on('ready', function () { 44 | console.log('[echo] ready'); 45 | const data = require('./testdata/ready.json'); 46 | jbSession.trigger('initActivity', data); 47 | }); 48 | 49 | jbSession.on('setActivityDirtyState', function (value) { 50 | console.log('[echo] setActivityDirtyState -> ', value); 51 | }); 52 | 53 | jbSession.on('requestInspectorClose', function () { 54 | console.log('[echo] requestInspectorClose'); 55 | }); 56 | 57 | jbSession.on('updateActivity', function (activity) { 58 | console.log( 59 | '[echo] updateActivity -> ', 60 | JSON.stringify(activity, null, 4) 61 | ); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/treeItem/__tests__/treeItem.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: MIT 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import { shadowQuerySelector } from 'lightning/testUtils'; 10 | import Element from 'c/treeItem'; 11 | 12 | describe('c-tree-item', () => { 13 | it('renders leaf tree-item', () => { 14 | const element = createElement('c-tree-item', { is: Element }); 15 | document.body.appendChild(element); 16 | element.isRoot = false; 17 | element.label = 'label'; 18 | element.href = 'href'; 19 | element.metatext = 'metatext'; 20 | element.nodeRef = { 21 | expanded: false 22 | }; 23 | 24 | element.isExpanded = false; 25 | element.isDisabled = false; 26 | element.nodename = '2234'; 27 | element.nodeKey = '1.1'; 28 | element.isLeaf = true; 29 | 30 | return Promise.resolve().then(() => { 31 | expect(element).toMatchSnapshot(); 32 | }); 33 | }); 34 | 35 | it('uses the correct icon for LTR', () => { 36 | const element = createElement('c-tree-item', { is: Element }); 37 | document.body.appendChild(element); 38 | 39 | element.isRoot = false; 40 | element.label = 'label'; 41 | element.href = 'href'; 42 | 43 | return Promise.resolve().then(() => { 44 | const lightningIcon = shadowQuerySelector( 45 | element, 46 | 'c-primitive-icon' 47 | ); 48 | 49 | expect(lightningIcon.iconName).toBe('utility:chevronright'); 50 | }); 51 | }); 52 | 53 | it('uses the correct icon for RTL', () => { 54 | const element = createElement('c-tree-item', { is: Element }); 55 | document.dir = 'rtl'; 56 | document.body.appendChild(element); 57 | 58 | element.isRoot = false; 59 | element.label = 'label'; 60 | element.href = 'href'; 61 | 62 | return Promise.resolve().then(() => { 63 | const lightningIcon = shadowQuerySelector( 64 | element, 65 | 'c-primitive-icon' 66 | ); 67 | 68 | expect(lightningIcon.iconName).toBe('utility:chevronleft'); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/keyboard.js: -------------------------------------------------------------------------------- 1 | export const keyCodes = { 2 | tab: 9, 3 | backspace: 8, 4 | enter: 13, 5 | escape: 27, 6 | space: 32, 7 | pageup: 33, 8 | pagedown: 34, 9 | end: 35, 10 | home: 36, 11 | left: 37, 12 | up: 38, 13 | right: 39, 14 | down: 40, 15 | delete: 46, 16 | shift: 16 17 | }; 18 | 19 | // Acceptable values are defined here: https://developer.mozilla.org/en-US/docs/Web/KeyboardEvent/key/Key_Values 20 | // remove this function when IE11 support is dropped 21 | export function normalizeKeyValue(value) { 22 | switch (value) { 23 | case 'Spacebar': 24 | return ' '; 25 | case 'Esc': 26 | return 'Escape'; 27 | case 'Del': 28 | return 'Delete'; 29 | case 'Left': 30 | return 'ArrowLeft'; 31 | case 'Right': 32 | return 'ArrowRight'; 33 | case 'Down': 34 | return 'ArrowDown'; 35 | case 'Up': 36 | return 'ArrowUp'; 37 | default: 38 | return value; 39 | } 40 | } 41 | 42 | const buffer = {}; 43 | 44 | export function isShiftMetaOrControlKey(event) { 45 | return event.shiftKey || event.metaKey || event.ctrlKey; 46 | } 47 | 48 | /** 49 | * Runs an action and passes the string of buffered keys typed within a short time period. 50 | * Use for type-ahead like functionality in menus, lists, comboboxes, and similar components. 51 | * 52 | * @param {CustomEvent} event A keyboard event 53 | * @param {Function} action function to run, it's passed the buffered text 54 | */ 55 | export function runActionOnBufferedTypedCharacters(event, action) { 56 | const letter = event.key; 57 | 58 | if (letter && letter.length > 1) { 59 | // Not an individual character/letter, but rather a special code (like Shift, Backspace, etc.) 60 | return; 61 | } 62 | 63 | // If we were going to clear what keys were typed, don't yet. 64 | if (buffer._clearBufferId) { 65 | clearTimeout(buffer._clearBufferId); 66 | } 67 | 68 | buffer._keyBuffer = buffer._keyBuffer || []; 69 | buffer._keyBuffer.push(letter); 70 | 71 | const matchText = buffer._keyBuffer.join('').toLowerCase(); 72 | 73 | action(matchText); 74 | 75 | // eslint-disable-next-line @lwc/lwc/no-async-operation 76 | buffer._clearBufferId = setTimeout(() => { 77 | buffer._keyBuffer = []; 78 | }, 700); 79 | } 80 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a deep copy of an object or array 3 | * @param {object|array} obj - item to be copied 4 | * @returns {object|array} copy of the item 5 | */ 6 | export function deepCopy(obj) { 7 | if (Object(obj) !== obj) { 8 | // primitives 9 | return obj; 10 | } 11 | if (obj instanceof Set) { 12 | return new Set(obj); 13 | } 14 | if (obj instanceof Date) { 15 | return new Date(obj); 16 | } 17 | if (typeof obj === 'function') { 18 | return obj.bind({}); 19 | } 20 | if (Array.isArray(obj)) { 21 | const obj2 = []; 22 | const len = obj.length; 23 | for (let i = 0; i < len; i++) { 24 | obj2.push(deepCopy(obj[i])); 25 | } 26 | return obj2; 27 | } 28 | const result = Object.create({}); 29 | let keys = Object.keys(obj); 30 | if (obj instanceof Error) { 31 | // Error properties are non-enumerable 32 | keys = Object.getOwnPropertyNames(obj); 33 | } 34 | 35 | const len = keys.length; 36 | for (let i = 0; i < len; i++) { 37 | const key = keys[i]; 38 | result[key] = deepCopy(obj[key]); 39 | } 40 | return result; 41 | } 42 | 43 | /** 44 | * Compare two arrays and return true if they are equal 45 | * @param {array} array1 - first array to compare 46 | * @param {array} array2 - second array to compare 47 | * @returns {boolean} if the arrays are identical 48 | */ 49 | export function arraysEqual(array1, array2) { 50 | // if either array is falsey, return false 51 | if (!array1 || !array2) { 52 | return false; 53 | } 54 | 55 | // if array lengths don't match, return false 56 | if (array1.length !== array2.length) { 57 | return false; 58 | } 59 | 60 | for (let index = 0; index < array1.length; index++) { 61 | // Check if we have nested arrays 62 | if (array1[index] instanceof Array && array2[index] instanceof Array) { 63 | // recurse into the nested arrays 64 | if (!arraysEqual(array1[index], array2[index])) { 65 | return false; 66 | } 67 | } else if (array1[index] !== array2[index]) { 68 | // Warning - two different object instances will never be equal: {x:20} != {x:20} 69 | return false; 70 | } 71 | } 72 | 73 | return true; 74 | } 75 | 76 | export const ArraySlice = Array.prototype.slice; 77 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/tree/treeDataGenerator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: MIT 5 | * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT 6 | */ 7 | 8 | let idCounter = 0; 9 | 10 | function generateUniqueId() { 11 | idCounter++; 12 | return `node-${idCounter}`; 13 | } 14 | 15 | export function generateNode( 16 | labelPrefix, 17 | expanded, 18 | disabled, 19 | withMetatext = false 20 | ) { 21 | const node = {}; 22 | node.name = generateUniqueId(); 23 | const computedLabel = labelPrefix + '_' + node.name; 24 | node.label = computedLabel; 25 | node.expanded = expanded ? expanded : false; 26 | node.disabled = disabled ? disabled : false; 27 | if (withMetatext) { 28 | node.metatext = 'meta · ' + node.name; 29 | } 30 | node.items = []; 31 | return node; 32 | } 33 | 34 | export function getTree(label, numChild, withMetatext = false) { 35 | const items = []; 36 | let node = null; 37 | for (let i = 0; i < numChild; i++) { 38 | node = generateNode(label, true, false, withMetatext); 39 | items.push(node); 40 | } 41 | return items; 42 | } 43 | 44 | export function getTreeNested(label, levelOptions, withMetatext = false) { 45 | const items = getTree(label, levelOptions['1'], withMetatext); 46 | addNodes(label, items, levelOptions, 2, withMetatext); 47 | return items; 48 | } 49 | 50 | export function getNode(items, key) { 51 | const childsByLevel = key.split('.'); 52 | let item = null; 53 | let parent = items; 54 | for (let i = 0; i < childsByLevel.length; i++) { 55 | if (parent && Array.isArray(parent)) { 56 | item = parent[childsByLevel[i] - 1]; 57 | parent = item.children; 58 | } 59 | } 60 | return item; 61 | } 62 | 63 | function addNodes(label, nodes, levelOptions, level, withMetatext = false) { 64 | const levelTotal = Object.keys(levelOptions).map((key) => 65 | Object.prototype.hasOwnProperty.call(levelOptions, key) 66 | ).length; 67 | if (level > levelTotal) { 68 | return; 69 | } 70 | nodes.forEach((node) => { 71 | const children = getTree(label, levelOptions[level], withMetatext); 72 | node.items = node.items.concat(children); 73 | addNodes(label, node.items, levelOptions, level + 1, withMetatext); 74 | }); 75 | level--; 76 | } 77 | -------------------------------------------------------------------------------- /src/client/modules/datatools/savemodal/savemodal.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | import { getCookieByName } from 'common/utils'; 3 | 4 | export default class SaveModal extends LightningElement { 5 | @api fields; 6 | @track filename = ''; 7 | @track errors; 8 | 9 | handlefilename(e) { 10 | this.filename = e.detail.value; 11 | } 12 | handleclose() { 13 | this.dispatchEvent( 14 | new CustomEvent('closesave', { 15 | bubbles: true, 16 | composed: true 17 | }) 18 | ); 19 | } 20 | async handlesave() { 21 | this.dispatchEvent( 22 | new CustomEvent('loading', { 23 | bubbles: true, 24 | composed: true, 25 | detail: true 26 | }) 27 | ); 28 | const saveMetadata = JSON.parse(JSON.stringify(this.fields)).map( 29 | (field) => { 30 | delete field.key; 31 | delete field.matchclass; 32 | delete field.matchcolour; 33 | delete field.match; 34 | return field; 35 | } 36 | ); 37 | const resData = await fetch('/dataTools/createDataExtension', { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'text/plain', 41 | 'xsrf-token': getCookieByName.call(this, 'XSRF-TOKEN') 42 | }, 43 | body: JSON.stringify({ 44 | name: this.filename, 45 | fields: saveMetadata 46 | }) 47 | }); 48 | if (resData.status === 200) { 49 | this.dispatchEvent( 50 | new CustomEvent('loading', { 51 | bubbles: true, 52 | composed: true, 53 | detail: false 54 | }) 55 | ); 56 | this.handleclose(); 57 | } else { 58 | const res = await resData.json(); 59 | this.errors = res.errors; 60 | this.dispatchEvent( 61 | new CustomEvent('error', { 62 | bubbles: true, 63 | composed: true, 64 | detail: res 65 | }) 66 | ); 67 | this.dispatchEvent( 68 | new CustomEvent('loading', { 69 | bubbles: true, 70 | composed: true, 71 | detail: false 72 | }) 73 | ); 74 | } 75 | console.log(saveMetadata); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '19 2 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /src/client/modules/datatools/app/app.html: -------------------------------------------------------------------------------- 1 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfmc-lwcactivity", 3 | "description": "Example App using LWC in SFMC", 4 | "version": "0.0.1", 5 | "author": "Douglas Midgley ", 6 | "bugs": "https://github.com/Douglas Midgley/sfmc-lwcactivity/issues", 7 | "dependencies": { 8 | "axios": "^0.21.1", 9 | "body-parser": "^1.19.0", 10 | "compression": "^1.7.4", 11 | "connect-redis": "^6.0.0", 12 | "csurf": "^1.11.0", 13 | "csvtojson": "^2.0.10", 14 | "express": "^4.17.1", 15 | "express-rate-limit": "^5.3.0", 16 | "express-session": "^1.17.2", 17 | "google-libphonenumber": "^3.2.22", 18 | "helmet": "^4.6.0", 19 | "ioredis": "^4.27.7", 20 | "jsforce": "^1.10.1", 21 | "jsonwebtoken": "^8.5.1", 22 | "moment": "^2.29.1", 23 | "morgan": "^1.10.0", 24 | "rate-limit-redis": "^2.1.0", 25 | "winston": "^3.3.3", 26 | "xml2js": "^0.4.23" 27 | }, 28 | "devDependencies": { 29 | "@lwc/synthetic-shadow": "^2.3.0", 30 | "@salesforce-ux/design-system": "^2.15.9", 31 | "clean-webpack-plugin": "^3.0.0", 32 | "copy-webpack-plugin": "^9.0.1", 33 | "dotenv": "^10.0.0", 34 | "eslint": "^7.32.0", 35 | "eslint-plugin-inclusive-language": "^2.1.1", 36 | "html-webpack-plugin": "^5.3.2", 37 | "lightning-base-components": "^1.13.1-alpha", 38 | "lwc-services": "^3.1.2", 39 | "lwc-webpack-plugin": "^2.0.1", 40 | "npm-run-all": "^4.1.5", 41 | "postmonger": "0.0.16", 42 | "prettier": "^2.3.2", 43 | "webpack": "5.50.0", 44 | "webpack-cli": "^4.8.0" 45 | }, 46 | "engines": { 47 | "node": ">=14.15.0", 48 | "npm": ">=6.14.8" 49 | }, 50 | "homepage": "https://github.com/Accenture/sfmc-customapp", 51 | "keywords": [ 52 | "lwc", 53 | "customactivity", 54 | "sfmc" 55 | ], 56 | "license": "Apache-2.0", 57 | "nodemonConfig": { 58 | "watch": [ 59 | "src/server/**/*.js" 60 | ], 61 | "ext": "js", 62 | "ignore": [ 63 | "src/**/*.spec.js", 64 | "src/**/*.test.js" 65 | ], 66 | "exec": "node ./src/server/api.js" 67 | }, 68 | "repository": "https://github.com/Accenture/sfmc-customapp", 69 | "scripts": { 70 | "build": "webpack --config webpack.config.js", 71 | "build:cert": "cd certificates && buildCert.bat", 72 | "lint": "eslint -c .eslintrc.json", 73 | "watch": "run-p watch:client watch:server", 74 | "watch:client": "webpack --watch --config webpack.config.js --progress", 75 | "watch:server": "nodemon", 76 | "start": "node src/server/api.js", 77 | "prettier": "prettier --write \"**/*.js\"" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/server/resources/ISO_639-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | "ab", 3 | "ae", 4 | "af", 5 | "ak", 6 | "am", 7 | "an", 8 | "ar", 9 | "as", 10 | "av", 11 | "ay", 12 | "az", 13 | "ba", 14 | "be", 15 | "bg", 16 | "bh", 17 | "bm", 18 | "bi", 19 | "bn", 20 | "bo", 21 | "br", 22 | "bs", 23 | "ca", 24 | "ce", 25 | "ch", 26 | "co", 27 | "cr", 28 | "cs", 29 | "cu", 30 | "cv", 31 | "cy", 32 | "da", 33 | "de", 34 | "dv", 35 | "dz", 36 | "ee", 37 | "el", 38 | "en", 39 | "eo", 40 | "es", 41 | "et", 42 | "eu", 43 | "fa", 44 | "ff", 45 | "fi", 46 | "fj", 47 | "fo", 48 | "fr", 49 | "fy", 50 | "ga", 51 | "gd", 52 | "gl", 53 | "gn", 54 | "gu", 55 | "gv", 56 | "ha", 57 | "he", 58 | "hi", 59 | "ho", 60 | "hr", 61 | "ht", 62 | "hu", 63 | "hy", 64 | "hz", 65 | "ia", 66 | "id", 67 | "ie", 68 | "ig", 69 | "ii", 70 | "ik", 71 | "io", 72 | "is", 73 | "it", 74 | "iu", 75 | "ja", 76 | "jv", 77 | "ka", 78 | "kg", 79 | "ki", 80 | "kj", 81 | "kk", 82 | "kl", 83 | "km", 84 | "kn", 85 | "ko", 86 | "kr", 87 | "ks", 88 | "ku", 89 | "kv", 90 | "kw", 91 | "ky", 92 | "la", 93 | "lb", 94 | "lg", 95 | "li", 96 | "ln", 97 | "lo", 98 | "lt", 99 | "lu", 100 | "lv", 101 | "mg", 102 | "mh", 103 | "mi", 104 | "mk", 105 | "ml", 106 | "mn", 107 | "mr", 108 | "ms", 109 | "mt", 110 | "my", 111 | "na", 112 | "nb", 113 | "nd", 114 | "ne", 115 | "ng", 116 | "nl", 117 | "nn", 118 | "no", 119 | "nr", 120 | "nv", 121 | "ny", 122 | "oc", 123 | "oj", 124 | "om", 125 | "or", 126 | "os", 127 | "pa", 128 | "pi", 129 | "pl", 130 | "ps", 131 | "pt", 132 | "qu", 133 | "rm", 134 | "rn", 135 | "ro", 136 | "ru", 137 | "rw", 138 | "sa", 139 | "sc", 140 | "sd", 141 | "se", 142 | "sg", 143 | "si", 144 | "sk", 145 | "sl", 146 | "sm", 147 | "sn", 148 | "so", 149 | "sq", 150 | "sr", 151 | "ss", 152 | "st", 153 | "su", 154 | "sv", 155 | "sw", 156 | "ta", 157 | "te", 158 | "tg", 159 | "th", 160 | "ti", 161 | "tk", 162 | "tl", 163 | "tn", 164 | "to", 165 | "tr", 166 | "ts", 167 | "tt", 168 | "tw", 169 | "ty", 170 | "ug", 171 | "uk", 172 | "ur", 173 | "uz", 174 | "ve", 175 | "vi", 176 | "vo", 177 | "wa", 178 | "wo", 179 | "xh", 180 | "yi", 181 | "yo", 182 | "za", 183 | "zh", 184 | "zu" 185 | ] 186 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/eventEmitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | An emitter implementation based on the Node.js EventEmitter API: 3 | https://nodejs.org/dist/latest-v6.x/docs/events.html#events_class_eventemitter 4 | **/ 5 | export class EventEmitter { 6 | constructor() { 7 | this.registry = {}; 8 | } 9 | 10 | /** 11 | Registers a listener on the emitter 12 | @method EventEmitter#on 13 | @param {String} name - The name of the event 14 | @param {Function} listener - The callback function 15 | @return {EventEmitter} - Returns a reference to the `EventEmitter` so that calls can be chained 16 | **/ 17 | on(name, listener) { 18 | this.registry[name] = this.registry[name] || []; 19 | this.registry[name].push(listener); 20 | return this; 21 | } 22 | 23 | /** 24 | Registers a listener on the emitter that only executes once 25 | @method EventEmitter#once 26 | @param {String} name - The name of the event 27 | @param {Function} listener - The callback function 28 | @return {EventEmitter} - Returns a reference to the `EventEmitter` so that calls can be chained 29 | **/ 30 | once(name, listener) { 31 | const doOnce = function () { 32 | listener.apply(null, arguments); 33 | this.removeListener(name, doOnce); 34 | }.bind(this); 35 | this.on(name, doOnce); 36 | return this; 37 | } 38 | 39 | /** 40 | Synchronously calls each listener registered with the specified event 41 | @method EventEmitter#emit 42 | @param {String} name - The name of the event 43 | @return {Boolean} - Returns `true` if the event had listeners, `false` otherwise 44 | **/ 45 | emit(name) { 46 | const args = Array.prototype.slice.call(arguments, 1); 47 | const listeners = this.registry[name]; 48 | let count = 0; 49 | 50 | if (listeners) { 51 | listeners.forEach((listener) => { 52 | count += 1; 53 | listener.apply(null, args); 54 | }); 55 | } 56 | return count > 0; 57 | } 58 | 59 | /** 60 | Removes the specified `listener` from the listener array for the event named `name` 61 | @method EventEmitter#removeListener 62 | @param {String} name - The name of the event 63 | @param {Function} listener - The callback function 64 | @return {EventEmitter} - Returns a reference to the `EventEmitter` so that calls can be chained 65 | **/ 66 | removeListener(name, listener) { 67 | const listeners = this.registry[name]; 68 | if (listeners) { 69 | for (let i = 0, len = listeners.length; i < len; i += 1) { 70 | if (listeners[i] === listener) { 71 | listeners.splice(i, 1); 72 | return this; 73 | } 74 | } 75 | } 76 | return this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/server/salesforceconfig/salesforceconfig-api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('../utils/logger'); 3 | const sfdc = require('../sfdc/index.js'); 4 | const router = express.Router({ strict: true }); 5 | const csurf = require('csurf')(); 6 | const { checkAuth, getRedirectURL } = require('../sfmc/core.js'); 7 | 8 | //default entry path with auth validation 9 | router.get('/app', csurf, (req, res, next) => { 10 | res.cookie('XSRF-TOKEN', req.csrfToken(), { 11 | sameSite: 'none', 12 | secure: true 13 | }); 14 | checkAuth(req, res, next, req.originalUrl.substring(1)); 15 | }); 16 | 17 | // path in case we want to force a refresh of token 18 | router.get('/app/login', csurf, (req, res) => { 19 | res.redirect( 20 | 301, 21 | getRedirectURL(req, req.originalUrl.replace('/login', '').substring(1)) 22 | ); 23 | }); 24 | 25 | router.get('/context', (req, res) => { 26 | res.json(req.session.context); 27 | }); 28 | router.post('/sfdccredentials', csurf, async (req, res) => { 29 | if ( 30 | req.session.context && 31 | req.session.context.organization && 32 | req.session.context.organization.member_id 33 | ) { 34 | req.session.temp = { 35 | mid: req.session.context.organization.member_id, 36 | cred: req.body 37 | }; 38 | const hostname = 39 | process.env.NODE_ENV === 'development' 40 | ? `127.0.0.1:${process.env.PORT}` 41 | : req.hostname; 42 | res.json({ 43 | redirect: sfdc.loginurl( 44 | req.body, 45 | hostname, 46 | req.session.context.organization.member_id, 47 | req.sessionID 48 | ) 49 | }); 50 | } else { 51 | logger.debug('sfdccredentials', req.session); 52 | res.status(500).json({ result: 'rejected, no context ' }); 53 | } 54 | }); 55 | router.get('/oauth/response/:mid', async (req, res) => { 56 | try { 57 | await sfdc.authorize(req.params.mid, req.query.code); 58 | delete req.session.temp; 59 | res.status(200).send( 60 | 'Finalizing Authorization. This window will close in a couple of seconds' 61 | ); 62 | } catch (ex) { 63 | res.status(500).json({ message: ex }); 64 | } 65 | }); 66 | 67 | router.get('/sfdcstatus', async (req, res) => { 68 | try { 69 | if ( 70 | req.session && 71 | req.session.context && 72 | req.session.context.organization 73 | ) { 74 | const obj = await sfdc.status( 75 | req.session.context.organization.member_id 76 | ); 77 | res.status(200).json(obj); 78 | } else { 79 | res.status(500).json({ 80 | message: 'Context not set', 81 | detail: req.session 82 | }); 83 | } 84 | } catch (ex) { 85 | res.status(500).json({ message: ex.message }); 86 | } 87 | }); 88 | 89 | module.exports = router; 90 | -------------------------------------------------------------------------------- /src/server/dataTools/dataTools-api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('../utils/logger'); 3 | const { checkAuth, getRedirectURL } = require('../sfmc/core.js'); 4 | const data = require('../sfmc/data.js'); 5 | const platform = require('../sfmc/platform.js'); 6 | const profiler = require('./profiler.js'); 7 | 8 | const router = express.Router(); 9 | 10 | //all POST routes here are csrf protected 11 | 12 | const csurf = require('csurf')(); 13 | router.use(csurf); 14 | 15 | router.get('/', (req, res, next) => { 16 | res.cookie('XSRF-TOKEN', req.csrfToken(), { 17 | sameSite: 'none', 18 | secure: true 19 | }); 20 | checkAuth(req, res, next, req.originalUrl); 21 | }); 22 | router.get('/login', (req, res) => { 23 | res.redirect(301, getRedirectURL(req, 'dataTools')); 24 | }); 25 | router.post('/exampledata', async (req, res) => { 26 | try { 27 | logger.info('locale', req.query); 28 | const metadata = await profiler.parse(req.body, req.query.phonelocale); 29 | res.status(200).json(metadata); 30 | } catch (ex) { 31 | res.status(500).json(ex.message); 32 | } 33 | }); 34 | 35 | router.get('/getDataExtensions', checkAuth, async (req, res) => { 36 | try { 37 | res.json(await data.getDataExtensions(req)); 38 | } catch (ex) { 39 | logger.error(ex); 40 | res.status(500).json(ex.message); 41 | } 42 | }); 43 | router.get('/getDataExtension/:key/fields', checkAuth, async (req, res) => { 44 | try { 45 | res.json(await data.getDataExtensionFields(req)); 46 | } catch (ex) { 47 | res.status(500).json(ex.message); 48 | } 49 | }); 50 | 51 | router.post('/getDataExtensionData', async (req, res) => { 52 | try { 53 | const fieldSet = req.body.fields.map((field) => field.fieldName); 54 | //add _CustomObjectKey 55 | fieldSet.push('_CustomObjectKey'); 56 | if (fieldSet) { 57 | const objectData = await data.getDataExtensionData( 58 | req, 59 | req.body.name, 60 | fieldSet 61 | ); 62 | if (objectData) { 63 | res.json(objectData); 64 | } else { 65 | res.status(204).json({ 66 | message: 'No Rows Found', 67 | status: 'warn' 68 | }); 69 | } 70 | } else { 71 | res.status(500).json({ 72 | message: 'No Fields Available', 73 | status: 'error' 74 | }); 75 | } 76 | } catch (ex) { 77 | logger.error(ex); 78 | res.status(500).json(ex); 79 | } 80 | }); 81 | 82 | router.get('/getFolders', checkAuth, async (req, res) => { 83 | try { 84 | res.json(await platform.getFolders(req)); 85 | } catch (ex) { 86 | res.status(500).json(ex.message); 87 | } 88 | }); 89 | router.post('/createDataExtension', checkAuth, async (req, res) => { 90 | try { 91 | res.json(await data.createDataExtension(req)); 92 | } catch (ex) { 93 | res.status(500).json(ex); 94 | } 95 | }); 96 | 97 | module.exports = router; 98 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/ready.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "id": "b58d71b3-66ef-4d3a-b549-2a4cdf8902b8", 4 | "key": "REST-2", 5 | "type": "REST", 6 | "arguments": { 7 | "executionMode": "{{Context.ExecutionMode}}", 8 | "definitionId": "{{Context.DefinitionId}}", 9 | "activityId": "{{Activity.Id}}", 10 | "contactKey": "{{Context.ContactKey}}", 11 | "execute": { 12 | "inArguments": [ 13 | { 14 | "type": "0ML5J000000PBVKWA4", 15 | "content": "Hi Doug", 16 | "recipient": "0055J000000lCFWQA2", 17 | "target": "0055J000000lCFWQA2", 18 | "mid": 7330184 19 | } 20 | ], 21 | "outArguments": [], 22 | "url": "https://sfmc-lwcactivity.herokuapp.com/salesforcenotification/execute", 23 | "timeout": 10000, 24 | "retryCount": 3, 25 | "retryDelay": 1000, 26 | "concurrentRequests": 5, 27 | "useJwt": true 28 | }, 29 | "testExecute": "", 30 | "startActivityKey": "{{Context.StartActivityKey}}", 31 | "definitionInstanceId": "{{Context.DefinitionInstanceId}}", 32 | "requestObjectId": "{{Context.RequestObjectId}}" 33 | }, 34 | "configurationArguments": { 35 | "save": { 36 | "url": "https://sfmc-lwcactivity.herokuapp.com/salesforcenotification/save", 37 | "useJwt": true 38 | }, 39 | "testSave": "", 40 | "publish": { 41 | "url": "https://sfmc-lwcactivity.herokuapp.com/salesforcenotification/publish", 42 | "useJwt": true 43 | }, 44 | "testPublish": "", 45 | "unpublish": "", 46 | "stop": { 47 | "url": "https://sfmc-lwcactivity.herokuapp.com/salesforcenotification/stop", 48 | "useJwt": true 49 | }, 50 | "testStop": "", 51 | "testUnpublish": "", 52 | "partnerActivityId": "", 53 | "validate": { 54 | "url": "https://sfmc-lwcactivity.herokuapp.com/salesforcenotification/validate", 55 | "useJwt": true 56 | }, 57 | "testValidate": "", 58 | "outArgumentSchema": {} 59 | }, 60 | "metaData": { 61 | "icon": "https://sfmc-lwcactivity.herokuapp.com/assets/notification.png", 62 | "category": "messaging", 63 | "backgroundColor": "#032e61", 64 | "expressionBuilderPrefix": "sfnotif", 65 | "iconSmall": "", 66 | "statsContactIcon": "", 67 | "original_icon": "https://sfmc-lwcactivity.herokuapp.com/assets/notification.png", 68 | "isConfigured": true 69 | }, 70 | "schema": { 71 | "arguments": { 72 | "execute": { 73 | "inArguments": [], 74 | "outArguments": [] 75 | } 76 | } 77 | }, 78 | "editable": false, 79 | "outcomes": [ 80 | { 81 | "key": "43d99f6f-e29f-454b-93ce-9163d065b5de", 82 | "next": "WAITBYDURATION-6", 83 | "arguments": {}, 84 | "metaData": { 85 | "invalid": false 86 | } 87 | } 88 | ], 89 | "errors": null 90 | } 91 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/primitiveIcon/fetch.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @lwc/lwc/no-async-await 2 | export async function fetchIconLibrary(dir, category) { 3 | if (dir === 'rtl') { 4 | switch (category) { 5 | case 'utility': { 6 | // eslint-disable-next-line @lwc/lwc/no-async-await 7 | const { default: Lib } = await import( 8 | 'lightning/iconSvgTemplatesUtilityRtl' 9 | ); 10 | return Lib; 11 | } 12 | case 'action': { 13 | // eslint-disable-next-line @lwc/lwc/no-async-await 14 | const { default: Lib } = await import( 15 | 'lightning/iconSvgTemplatesActionRtl' 16 | ); 17 | return Lib; 18 | } 19 | case 'standard': { 20 | // eslint-disable-next-line @lwc/lwc/no-async-await 21 | const { default: Lib } = await import( 22 | 'lightning/iconSvgTemplatesStandardRtl' 23 | ); 24 | return Lib; 25 | } 26 | case 'doctype': { 27 | // eslint-disable-next-line @lwc/lwc/no-async-await 28 | const { default: Lib } = await import( 29 | 'lightning/iconSvgTemplatesDoctypeRtl' 30 | ); 31 | return Lib; 32 | } 33 | case 'custom': { 34 | // eslint-disable-next-line @lwc/lwc/no-async-await 35 | const { default: Lib } = await import( 36 | 'lightning/iconSvgTemplatesCustomRtl' 37 | ); 38 | return Lib; 39 | } 40 | default: 41 | return null; 42 | } 43 | } else { 44 | switch (category) { 45 | case 'utility': { 46 | // eslint-disable-next-line @lwc/lwc/no-async-await 47 | const { default: Lib } = await import( 48 | 'lightning/iconSvgTemplatesUtility' 49 | ); 50 | return Lib; 51 | } 52 | case 'action': { 53 | // eslint-disable-next-line @lwc/lwc/no-async-await 54 | const { default: Lib } = await import( 55 | 'lightning/iconSvgTemplatesAction' 56 | ); 57 | return Lib; 58 | } 59 | case 'standard': { 60 | // eslint-disable-next-line @lwc/lwc/no-async-await 61 | const { default: Lib } = await import( 62 | 'lightning/iconSvgTemplatesStandard' 63 | ); 64 | return Lib; 65 | } 66 | case 'doctype': { 67 | // eslint-disable-next-line @lwc/lwc/no-async-await 68 | const { default: Lib } = await import( 69 | 'lightning/iconSvgTemplatesDoctype' 70 | ); 71 | return Lib; 72 | } 73 | case 'custom': { 74 | // eslint-disable-next-line @lwc/lwc/no-async-await 75 | const { default: Lib } = await import( 76 | 'lightning/iconSvgTemplatesCustom' 77 | ); 78 | return Lib; 79 | } 80 | default: 81 | return null; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const LwcWebpackPlugin = require('lwc-webpack-plugin'); 5 | const path = require('path'); 6 | 7 | const config = { 8 | entry: { 9 | fallback: './src/client/index.js', 10 | dataTools: './src/client/dataTools.js', 11 | salesforceconfig: './src/client/salesforceconfig.js', 12 | platformeventactivity: './src/client/platformeventactivity.js', 13 | salesforcenotification: './src/client/salesforcenotification.js' 14 | }, 15 | mode: 'production', 16 | output: { 17 | path: path.resolve('dist'), 18 | filename: './[name].js' 19 | }, 20 | plugins: [ 21 | new CleanWebpackPlugin(), 22 | new LwcWebpackPlugin({ 23 | modules: [ 24 | { dir: 'src/client/modules' }, 25 | { npm: 'lightning-base-components' } 26 | ] 27 | }), 28 | new HtmlWebpackPlugin({ 29 | template: 'src/client/index.html', 30 | filename: './index.html', 31 | title: 'fallback', 32 | chunks: ['fallback'] 33 | }), 34 | new HtmlWebpackPlugin({ 35 | template: 'src/client/index.html', 36 | filename: './dataTools.html', 37 | title: 'Data Tools', 38 | chunks: ['dataTools'] 39 | }), 40 | 41 | new HtmlWebpackPlugin({ 42 | template: 'src/client/index.html', 43 | filename: './salesforceconfig/app.html', 44 | title: 'Salesforce Config', 45 | chunks: ['salesforceconfig'] 46 | }), 47 | new HtmlWebpackPlugin({ 48 | template: 'src/client/index.html', 49 | filename: './platformevent/activity.html', 50 | title: 'Platform Event Activity', 51 | chunks: ['platformeventactivity'] 52 | }), 53 | new HtmlWebpackPlugin({ 54 | template: 'src/client/index.html', 55 | filename: './salesforcenotification/activity.html', 56 | title: 'Salesforce Notification Activity', 57 | chunks: ['salesforcenotification'] 58 | }), 59 | new CopyPlugin({ 60 | patterns: [ 61 | { 62 | from: 'src/client/assets', 63 | to: 'assets/' 64 | }, 65 | { 66 | from: 'node_modules/@salesforce-ux/design-system/assets/images', 67 | to: 'assets/images' 68 | }, 69 | { 70 | from: 'node_modules/@salesforce-ux/design-system/assets/icons', 71 | to: 'assets/icons' 72 | }, 73 | { 74 | from: 'node_modules/@salesforce-ux/design-system/assets/styles/salesforce-lightning-design-system.min.css', 75 | to: 'assets/styles/salesforce-lightning-design-system.min.css' 76 | } 77 | ] 78 | }) 79 | ], 80 | stats: { assets: false } 81 | }; 82 | 83 | // development only 84 | if (process.env.NODE_ENV !== 'production') { 85 | require('dotenv').config(); 86 | config.mode = 'development'; 87 | config.devtool = 'source-map'; 88 | config.watchOptions = { 89 | ignored: /node_modules/, 90 | aggregateTimeout: 5000 91 | }; 92 | } 93 | 94 | module.exports = config; 95 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/aria.js: -------------------------------------------------------------------------------- 1 | /* All Valid Aria Attributes, in camel case 2 | * - it's better to start from camel-case 3 | * because `aria-${_.kebabCase('describedBy')}` => 'aria-described-by' (NOT aria property) 4 | * - correct aria property: 'aria-describedby' 5 | * https://www.w3.org/TR/wai-aria/ 6 | */ 7 | const ARIA_PROP_LIST = [ 8 | 'activeDescendant', 9 | 'atomic', 10 | 'autoComplete', 11 | 'busy', 12 | 'checked', 13 | 'colCount', 14 | 'colIndex', 15 | 'colSpan', 16 | 'controls', 17 | 'current', 18 | 'describedAt', 19 | 'describedBy', 20 | 'details', 21 | 'disabled', 22 | 'dropEffect', 23 | 'errorMessage', 24 | 'expanded', 25 | 'flowTo', 26 | 'grabbed', 27 | 'hasPopup', 28 | 'hidden', 29 | 'invalid', 30 | 'keyShortcuts', 31 | 'label', 32 | 'labelledBy', 33 | 'level', 34 | 'live', 35 | 'modal', 36 | 'multiLine', 37 | 'multiSelectable', 38 | 'orientation', 39 | 'owns', 40 | 'placeholder', 41 | 'posInSet', 42 | 'pressed', 43 | 'readOnly', 44 | 'relevant', 45 | 'required', 46 | 'roleDescription', 47 | 'rowCount', 48 | 'rowIndex', 49 | 'rowSpan', 50 | 'selected', 51 | 'setSize', 52 | 'sort', 53 | 'valueMax', 54 | 'valueMin', 55 | 'valueNow', 56 | 'valueText' 57 | ]; 58 | 59 | /** 60 | * Generate an ARIA lookup object when passing in a list of ARIA values 61 | * @param {Array} list A list of ARIA properties (array of strings) 62 | * @param {String} type A type which defaults to output ARIA properties as modified kebab-case, or camel-case 63 | * @example 'valueMax' string becomes: { VALUEMAX: 'aria-valuemax' } 64 | * @returns {Object} A lookup object for ARIA properties in (modified) kebab-case or camel-case 65 | */ 66 | const getAriaLookup = (list, type = 'default') => { 67 | if (!list || list.length === 0) { 68 | throw new Error('List of aria properties is required'); 69 | } 70 | const lookupObj = {}; 71 | if (type === 'default') { 72 | list.forEach((name) => { 73 | const nameUpperCase = name.toUpperCase(); 74 | if (!lookupObj[nameUpperCase]) { 75 | lookupObj[nameUpperCase] = `aria-${name.toLowerCase()}`; 76 | } 77 | }); 78 | return lookupObj; 79 | } 80 | list.forEach((name) => { 81 | const ariaPropertyLowerCase = `aria-${name.toLowerCase()}`; 82 | const ariaPropertyCamelCase = `aria${name 83 | .charAt(0) 84 | .toUpperCase()}${name.slice(1)}`; 85 | if (!lookupObj[ariaPropertyLowerCase]) { 86 | lookupObj[ariaPropertyLowerCase] = ariaPropertyCamelCase; 87 | } 88 | }); 89 | return lookupObj; 90 | }; 91 | 92 | /** 93 | * ARIA lookup, 'modified' kebab-case 94 | * Given an array of aria property strings in camel-case, produce a lookup object 95 | * NOTE: 'ariaDescribedBy' (camel-case ARIA property) in TRUE kebab-case would be: 96 | * - 'aria-described-by' (not valid ARIA) 97 | * - 'aria-describedby' (valid ARIA, or modified kebab-case) 98 | * Example: 'describedBy' -> { DESCRIBEDBY: 'aria-describedby' } 99 | */ 100 | export const ARIA = getAriaLookup(ARIA_PROP_LIST); 101 | 102 | /** 103 | * ARIA lookup, aria-property (key): 'ariaCamelCase' (value) 104 | * Example: 'valueMax' -> { 'aria-valuemax': 'ariaValueMax' } 105 | * Useful for converting from normal aria properties to aria camel cased values 106 | */ 107 | export const ARIA_TO_CAMEL = getAriaLookup(ARIA_PROP_LIST, 'cc'); 108 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/linkify.js: -------------------------------------------------------------------------------- 1 | import { 2 | urlRegexString, 3 | newLineRegexString, 4 | emailRegexString, 5 | createHttpHref, 6 | createEmailHref 7 | } from './linkUtils'; 8 | 9 | /* 10 | * Regex was taken from aura lib and refactored 11 | */ 12 | const linkRegex = new RegExp( 13 | `(${newLineRegexString})|${urlRegexString}|${emailRegexString}`, 14 | 'gi' 15 | ); 16 | const linkRegexNoNewLine = new RegExp( 17 | `${urlRegexString}|${emailRegexString}`, 18 | 'gi' 19 | ); 20 | const emailRegex = new RegExp(emailRegexString, 'gi'); 21 | const newLineRegex = new RegExp(newLineRegexString, 'gi'); 22 | 23 | function getTextPart(text) { 24 | return { 25 | isText: true, 26 | value: text 27 | }; 28 | } 29 | 30 | function getUrlPart(url, index) { 31 | return { 32 | isLink: true, 33 | value: url, 34 | href: createHttpHref(url), 35 | key: `${url}-${index}` 36 | }; 37 | } 38 | 39 | function getEmailPart(email, index) { 40 | return { 41 | isLink: true, 42 | value: email, 43 | href: createEmailHref(email), 44 | key: `${email}-${index}` 45 | }; 46 | } 47 | 48 | function getNewlinePart(index) { 49 | return { 50 | isNewline: true, 51 | key: index 52 | }; 53 | } 54 | 55 | function getLinkPart(link, index, ignoreNewLines) { 56 | if (!ignoreNewLines && link.match(newLineRegex)) { 57 | return getNewlinePart(index); 58 | } else if (link.match(emailRegex)) { 59 | return getEmailPart(link, index); 60 | } 61 | return getUrlPart(link, index); 62 | } 63 | 64 | /** 65 | * Parse text into parts of text, links, emails, new lines 66 | * @param text {string} Text to parse into linkified parts 67 | * @param ignoreNewLines {boolean} Boolean indicating whether to return new line parts or not 68 | * if true new lines are included in text/url/email parts otherwise they are returned in their 69 | * own parts by default 70 | * @returns {[]} 71 | */ 72 | export function parseToFormattedLinkifiedParts(text, ignoreNewLines = false) { 73 | const parts = []; 74 | const re = ignoreNewLines ? linkRegexNoNewLine : linkRegex; 75 | let match; 76 | let index = 0; 77 | while ((match = re.exec(text)) !== null) { 78 | let link = match[0]; 79 | const endsWithQuote = link && link.endsWith('"'); 80 | // If we found an email or url match, then create a text part for everything 81 | // up to the match and then create the part for the email or url 82 | if (match.index > 0) { 83 | parts.push(getTextPart(text.slice(0, match.index))); 84 | } 85 | if (endsWithQuote) { 86 | link = link.slice(0, link.lastIndexOf('"')); 87 | } 88 | parts.push(getLinkPart(link, index, ignoreNewLines)); 89 | 90 | if (endsWithQuote) { 91 | parts.push(getTextPart('"')); 92 | } 93 | text = text.substring(re.lastIndex); 94 | re.lastIndex = 0; 95 | index = index + 1; 96 | } 97 | if (text != null && text !== '') { 98 | parts.push(getTextPart(text)); 99 | } 100 | return parts; 101 | } 102 | 103 | /** 104 | * Parse text into parts of text and new lines 105 | * @param text {string} Text to parse into parts 106 | * @returns {[]} 107 | */ 108 | export function parseToFormattedParts(text) { 109 | return text.split(newLineRegex).map((part, index) => { 110 | return index % 2 === 0 ? getTextPart(part) : getNewlinePart(); 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /src/client/modules/salesforceconfig/app/app.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track } from 'lwc'; 2 | import { getCookieByName } from 'common/utils'; 3 | 4 | export default class Config extends LightningElement { 5 | @track config = { clientId: '', loginUrl: '' }; 6 | @track isEditing = false; 7 | @track isLoading = true; 8 | 9 | onsfdcurlchange(e) { 10 | this.config.sfdcurl = e.detail.value; 11 | } 12 | onsfdcclientidchange(e) { 13 | this.config.sfdcclientid = e.detail.value; 14 | } 15 | onsfdcclientsecretchange(e) { 16 | this.config.sfdcclientsecret = e.detail.value; 17 | } 18 | toggleEdit() { 19 | this.isEditing = !this.isEditing; 20 | } 21 | async handleConnect() { 22 | this.isLoading = true; 23 | const rawRes = await fetch('/salesforceconfig/sfdccredentials', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'xsrf-token': getCookieByName.call(this, 'XSRF-TOKEN') 28 | }, 29 | redirect: 'follow', 30 | body: JSON.stringify(this.config) 31 | }); 32 | 33 | const jsonRes = await rawRes.json(); 34 | if (rawRes.status < 300) { 35 | const popup = window.open( 36 | jsonRes.redirect, 37 | 'sfdc_login', 38 | 'width=500,height=500' 39 | ); 40 | // we are running a check every 3 seconds if the popup is 41 | // complete since it was easier to implement than having a 42 | // whole other screen to return the values 43 | // eslint-disable-next-line @lwc/lwc/no-async-operation 44 | const checkComplete = setInterval(() => { 45 | if ( 46 | popup.location.href.includes( 47 | '/salesforceconfig/oauth/response/' 48 | ) 49 | ) { 50 | popup.close(); 51 | clearInterval(checkComplete); 52 | this.isLoading = false; 53 | this.isEditing = false; 54 | } 55 | }, 1000); 56 | } else { 57 | this.dispatchEvent( 58 | new CustomEvent('error', { 59 | bubbles: true, 60 | detail: { 61 | type: 'error', 62 | message: jsonRes 63 | } 64 | }) 65 | ); 66 | this.isLoading = false; 67 | } 68 | } 69 | async connectedCallback() { 70 | this.isLoading = true; 71 | const rawRes = await fetch('/salesforceconfig/sfdcstatus'); 72 | const jsonRes = await rawRes.json(); 73 | if (rawRes.status < 300) { 74 | if (jsonRes) { 75 | this.config = { 76 | sfdcclientid: jsonRes.clientId, 77 | sfdcurl: jsonRes.loginUrl, 78 | username: jsonRes.username, 79 | organization_id: jsonRes.organization_id 80 | }; 81 | if (!jsonRes.clientId) { 82 | this.isEditing = true; 83 | } 84 | } 85 | } else { 86 | this.dispatchEvent( 87 | new CustomEvent('error', { 88 | bubbles: true, 89 | detail: { 90 | type: 'error', 91 | message: jsonRes 92 | } 93 | }) 94 | ); 95 | } 96 | this.isLoading = false; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/client/modules/platformevent/activity/activity.html: -------------------------------------------------------------------------------- 1 | 85 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/testdata/requestDataSources.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Event", 4 | "name": "Entry: lWC", 5 | "eventDefinitionKey": "DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3", 6 | "keyPrefix": "Event.DEAudience-eabac269-8abc-2a8b-aebb-56d0728fdfb3.", 7 | "schema": null, 8 | "deSchema": { 9 | "links": {}, 10 | "fieldCount": 3, 11 | "fields": [ 12 | { 13 | "defaultValue": "secondValye", 14 | "description": "", 15 | "id": "e175a7f5-36ee-4845-89f9-df73137abdf9", 16 | "isHidden": false, 17 | "isInheritable": false, 18 | "isNullable": false, 19 | "isOverridable": false, 20 | "isPrimaryKey": true, 21 | "isReadOnly": false, 22 | "isTemplateField": false, 23 | "length": 50, 24 | "masktype": "None", 25 | "mustOverride": false, 26 | "name": "ContactKey", 27 | "ordinal": 0, 28 | "storagetype": "Plain", 29 | "type": "Text" 30 | }, 31 | { 32 | "description": "", 33 | "id": "e1b8d7ba-ed0f-4dad-978a-e4babe9ded47", 34 | "isHidden": false, 35 | "isInheritable": false, 36 | "isNullable": true, 37 | "isOverridable": false, 38 | "isPrimaryKey": false, 39 | "isReadOnly": false, 40 | "isTemplateField": false, 41 | "length": 254, 42 | "masktype": "None", 43 | "mustOverride": false, 44 | "name": "Email", 45 | "ordinal": 1, 46 | "storagetype": "Plain", 47 | "type": "EmailAddress" 48 | }, 49 | { 50 | "defaultValue": "happy", 51 | "description": "", 52 | "id": "10e167ae-8cf4-42de-bbe8-cb57823618d4", 53 | "isHidden": false, 54 | "isInheritable": false, 55 | "isNullable": true, 56 | "isOverridable": false, 57 | "isPrimaryKey": false, 58 | "isReadOnly": false, 59 | "isTemplateField": false, 60 | "length": 50, 61 | "masktype": "None", 62 | "mustOverride": false, 63 | "name": "restrictedvalue", 64 | "ordinal": 2, 65 | "storagetype": "Plain", 66 | "type": "Text" 67 | } 68 | ], 69 | "id": "209a7846-2294-ea11-a2e6-1402ec938a35" 70 | }, 71 | "dataExtensionId": "209a7846-2294-ea11-a2e6-1402ec938a35" 72 | }, 73 | { 74 | "id": "REST-1", 75 | "name": "Platform: Platform Event", 76 | "keyPrefix": "Interaction.REST-1.", 77 | "schema": { 78 | "fields": [ 79 | { 80 | "name": "discountCode", 81 | "dataType": "Text", 82 | "isNullable": false 83 | }, 84 | { 85 | "name": "discount", 86 | "dataType": "Number", 87 | "isNullable": false 88 | } 89 | ] 90 | } 91 | }, 92 | { 93 | "id": "START-1", 94 | "name": "", 95 | "keyPrefix": "Interaction.START-1.", 96 | "schema": { 97 | "fields": [] 98 | } 99 | } 100 | ] 101 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/iconUtils.js: -------------------------------------------------------------------------------- 1 | import { getPathPrefix, getToken } from 'lightning/configProvider'; 2 | import isIframeInEdge from './isIframeInEdge'; 3 | 4 | const validNameRe = /^([a-zA-Z]+):([a-zA-Z]\w*)$/; 5 | const underscoreRe = /_/g; 6 | 7 | let pathPrefix; 8 | 9 | const tokenNameMap = Object.assign(Object.create(null), { 10 | action: 'lightning.actionSprite', 11 | custom: 'lightning.customSprite', 12 | doctype: 'lightning.doctypeSprite', 13 | standard: 'lightning.standardSprite', 14 | utility: 'lightning.utilitySprite' 15 | }); 16 | 17 | const tokenNameMapRtl = Object.assign(Object.create(null), { 18 | action: 'lightning.actionSpriteRtl', 19 | custom: 'lightning.customSpriteRtl', 20 | doctype: 'lightning.doctypeSpriteRtl', 21 | standard: 'lightning.standardSpriteRtl', 22 | utility: 'lightning.utilitySpriteRtl' 23 | }); 24 | 25 | const defaultTokenValueMap = Object.assign(Object.create(null), { 26 | 'lightning.actionSprite': '/assets/icons/action-sprite/svg/symbols.svg', 27 | 'lightning.actionSpriteRtl': '/assets/icons/action-sprite/svg/symbols.svg', 28 | 'lightning.customSprite': '/assets/icons/custom-sprite/svg/symbols.svg', 29 | 'lightning.customSpriteRtl': '/assets/icons/custom-sprite/svg/symbols.svg', 30 | 'lightning.doctypeSprite': '/assets/icons/doctype-sprite/svg/symbols.svg', 31 | 'lightning.doctypeSpriteRtl': 32 | '/assets/icons/doctype-sprite/svg/symbols.svg', 33 | 'lightning.standardSprite': '/assets/icons/standard-sprite/svg/symbols.svg', 34 | 'lightning.standardSpriteRtl': 35 | '/assets/icons/standard-sprite/svg/symbols.svg', 36 | 'lightning.utilitySprite': '/assets/icons/utility-sprite/svg/symbols.svg', 37 | 'lightning.utilitySpriteRtl': '/assets/icons/utility-sprite/svg/symbols.svg' 38 | }); 39 | 40 | const getDefaultBaseIconPath = (category, nameMap) => 41 | defaultTokenValueMap[nameMap[category]]; 42 | 43 | const getBaseIconPath = (category, direction) => { 44 | const nameMap = direction === 'rtl' ? tokenNameMapRtl : tokenNameMap; 45 | return ( 46 | getToken(nameMap[category]) || getDefaultBaseIconPath(category, nameMap) 47 | ); 48 | }; 49 | 50 | const getMatchAtIndex = (index) => (iconName) => { 51 | const result = validNameRe.exec(iconName); 52 | return result ? result[index] : ''; 53 | }; 54 | 55 | const getCategory = getMatchAtIndex(1); 56 | const getName = getMatchAtIndex(2); 57 | export { getCategory, getName }; 58 | 59 | export const isValidName = (iconName) => validNameRe.test(iconName); 60 | 61 | export const getIconPath = (iconName, direction = 'ltr') => { 62 | pathPrefix = pathPrefix !== undefined ? pathPrefix : getPathPrefix(); 63 | 64 | if (isValidName(iconName)) { 65 | const baseIconPath = getBaseIconPath(getCategory(iconName), direction); 66 | if (baseIconPath) { 67 | // This check was introduced the following MS-Edge issue: 68 | // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/9655192/ 69 | // If and when this get fixed, we can safely remove this block of code. 70 | if (isIframeInEdge) { 71 | // protocol => 'https:' or 'http:' 72 | // host => hostname + port 73 | const origin = `${window.location.protocol}//${window.location.host}`; 74 | return `${origin}${pathPrefix}${baseIconPath}#${getName( 75 | iconName 76 | )}`; 77 | } 78 | return `${pathPrefix}${baseIconPath}#${getName(iconName)}`; 79 | } 80 | } 81 | return ''; 82 | }; 83 | 84 | export const computeSldsClass = (iconName) => { 85 | if (isValidName(iconName)) { 86 | const category = getCategory(iconName); 87 | const name = getName(iconName).replace(underscoreRe, '-'); 88 | return `slds-icon-${category}-${name}`; 89 | } 90 | return ''; 91 | }; 92 | 93 | export { polyfill } from './polyfill'; 94 | -------------------------------------------------------------------------------- /src/client/modules/salesforceconfig/app/app.html: -------------------------------------------------------------------------------- 1 | 90 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/utilsPrivate/linkUtils.js: -------------------------------------------------------------------------------- 1 | export const urlRegexString = 2 | "((?:(?:https?|ftp):\\/\\/(?:[\\w\\-\\|=%~#\\/+*@\\.,;:\\?!']|&){0,2047}(?:[\\(\\)\\.\\w=\\/+#-]*)[^\\s()\\.<>,;\\[\\]`'\"])|(?:\\b(?:[a-z0-9](?:[-a-z0-9]{0,62}[a-z0-9])?\\.)+(?:AC|AD|AE|AERO|AF|AG|AI|AL|AM|AN|AO|AQ|AR|ARPA|AS|ASIA|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BIZ|BJ|BM|BN|BO|BR|BS|BT|BV|BW|BY|BZ|CA|CAT|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|COM|COOP|CR|CU|CV|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EDU|EE|EG|ER|ES|ET|EU|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GOV|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|INFO|INT|IO|IQ|IR|IS|IT|JE|JM|JO|JOBS|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MG|MH|MIL|MK|ML|MM|MN|MO|MOBI|MP|MQ|MR|MS|MT|MU|MUSEUM|MV|MW|MX|MY|MZ|NA|NAME|NC|NE|NET|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|ORG|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PRO|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|ST|SU|SV|SY|SZ|TC|TD|TEL|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TP|TR|TRAVEL|TT|TV|TW|TZ|UA|UG|UK|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|XN--0ZWM56D|XN--11B5BS3A9AJ6G|XN--80AKHBYKNJ4F|XN--9T4B11YI5A|XN--DEBA0AD|XN--FIQS8S|XN--FIQZ9S|XN--G6W251D|XN--HGBK6AJ7F53BBA|XN--HLCJ6AYA9ESC7A|XN--J6W193G|XN--JXALPDLP|XN--KGBECHTV|XN--KPRW13D|XN--KPRY57D|XN--MGBAAM7A8H|XN--MGBERP4A5D4AR|XN--P1AI|XN--WGBH1C|XN--ZCKZAH|YE|YT|ZA|ZM|ZW)(?!@(?:[a-z0-9](?:[-a-z0-9]{0,62}[a-z0-9])?\\.)+(?:AC|AD|AE|AERO|AF|AG|AI|AL|AM|AN|AO|AQ|AR|ARPA|AS|ASIA|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BIZ|BJ|BM|BN|BO|BR|BS|BT|BV|BW|BY|BZ|CA|CAT|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|COM|COOP|CR|CU|CV|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EDU|EE|EG|ER|ES|ET|EU|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GOV|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|INFO|INT|IO|IQ|IR|IS|IT|JE|JM|JO|JOBS|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MG|MH|MIL|MK|ML|MM|MN|MO|MOBI|MP|MQ|MR|MS|MT|MU|MUSEUM|MV|MW|MX|MY|MZ|NA|NAME|NC|NE|NET|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|ORG|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PRO|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|ST|SU|SV|SY|SZ|TC|TD|TEL|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TP|TR|TRAVEL|TT|TV|TW|TZ|UA|UG|UK|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|XN--0ZWM56D|XN--11B5BS3A9AJ6G|XN--80AKHBYKNJ4F|XN--9T4B11YI5A|XN--DEBA0AD|XN--FIQS8S|XN--FIQZ9S|XN--G6W251D|XN--HGBK6AJ7F53BBA|XN--HLCJ6AYA9ESC7A|XN--J6W193G|XN--JXALPDLP|XN--KGBECHTV|XN--KPRW13D|XN--KPRY57D|XN--MGBAAM7A8H|XN--MGBERP4A5D4AR|XN--P1AI|XN--WGBH1C|XN--ZCKZAH|YE|YT|ZA|ZM|ZW))(?:/[\\w\\-=?/.&;:%~,+@#*]{0,2048}(?:[\\w=/+#-]|\\([^\\s()]*\\)))?(?:$|(?=\\.$)|(?=\\.\\s)|(?=[^\\w\\.]))))"; 3 | 4 | export const emailRegexString = 5 | '([\\w-\\.\\+_]{1,64}@(?:[\\w-]){1,255}(?:\\.[\\w-]{1,255}){1,10})'; 6 | 7 | export const newLineRegexString = '(\r\n|\r|\n)'; 8 | 9 | export const tagRegexString = 10 | '(]+[^/]>[\\s\\S]*?|]+/>|' + 11 | ']+[^/]>[\\s\\S]*?|]+/>|' + 12 | ']+[^/]>[\\s\\S]*?|]+/>|' + 13 | ']+[^/]>[\\s\\S]*?|]+/>|' + 14 | ']+[^/]>[\\s\\S]*?|]+>|' + 15 | ']+[^/]>[\\s\\S]*?|]+/>|' + 16 | ']+[^/]>[\\s\\S]*?|]+/>|' + 17 | ']+[^/]>[\\s\\S]*?|]+/>|' + 18 | ']+[^/]>[\\s\\S]*?|]+/>|' + 19 | ']+[^/]>[\\s\\S]*?|]+/>|' + 20 | ']+[^/]>[\\s\\S]*?|]+/>|' + 21 | ']+[^/]>[\\s\\S]*?|]+/>|' + 22 | ']+[^/]>[\\s\\S]*?|]+/>|' + 23 | ']+[^/]>[\\s\\S]*?|]+/>)'; 24 | 25 | export const createHttpHref = function (url) { 26 | let href = url; 27 | if ( 28 | url.toLowerCase().lastIndexOf('http', 0) !== 0 && 29 | url.toLowerCase().lastIndexOf('ftp', 0) !== 0 30 | ) { 31 | href = `http://${href}`; 32 | } 33 | return href; 34 | }; 35 | 36 | export const createEmailHref = function (email) { 37 | return `mailto:${email}`; 38 | }; 39 | -------------------------------------------------------------------------------- /src/client/modules/lightningcustom/iconUtils/polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | This polyfill injects SVG sprites into the document for clients that don't 3 | fully support SVG. We do this globally at the document level for performance 4 | reasons. This causes us to lose namespacing of IDs across sprites. For example, 5 | if both #image from utility sprite and #image from doctype sprite need to be 6 | rendered on the page, both end up as #image from the doctype sprite (last one 7 | wins). SLDS cannot change their image IDs due to backwards-compatibility 8 | reasons so we take care of this issue at runtime by adding namespacing as we 9 | polyfill SVG elements. 10 | 11 | For example, given "/assets/icons/action-sprite/svg/symbols.svg#approval", we 12 | replace the "#approval" id with "#${namespace}-approval" and a similar 13 | operation is done on the corresponding symbol element. 14 | **/ 15 | 16 | import fetchSvg from './fetchSvg'; 17 | import supportsSvg from './supportsSvg'; 18 | 19 | const svgTagName = /svg/i; 20 | const isSvgElement = (el) => el && svgTagName.test(el.nodeName); 21 | 22 | const requestCache = {}; 23 | const symbolEls = {}; 24 | const svgFragments = {}; 25 | 26 | const spritesContainerId = 'slds-svg-sprites'; 27 | let spritesEl; 28 | 29 | export function polyfill(el) { 30 | if (!supportsSvg && isSvgElement(el)) { 31 | if (!spritesEl) { 32 | spritesEl = document.createElement('svg'); 33 | spritesEl.xmlns = 'http://www.w3.org/2000/svg'; 34 | spritesEl['xmlns:xlink'] = 'http://www.w3.org/1999/xlink'; 35 | spritesEl.style.display = 'none'; 36 | spritesEl.id = spritesContainerId; 37 | 38 | document.body.insertBefore(spritesEl, document.body.childNodes[0]); 39 | } 40 | 41 | Array.from(el.getElementsByTagName('use')).forEach((use) => { 42 | // We access the href differently in raptor and in aura, probably 43 | // due to difference in the way the svg is constructed. 44 | const src = 45 | use.getAttribute('xlink:href') || use.getAttribute('href'); 46 | 47 | if (src) { 48 | // "/assets/icons/action-sprite/svg/symbols.svg#approval" => 49 | // ["/assets/icons/action-sprite/svg/symbols.svg", "approval"] 50 | const parts = src.split('#'); 51 | const url = parts[0]; 52 | const id = parts[1]; 53 | const namespace = url.replace(/[^\w]/g, '-'); 54 | const href = `#${namespace}-${id}`; 55 | 56 | if (url.length) { 57 | // set the HREF value to no longer be an external reference 58 | if (use.getAttribute('xlink:href')) { 59 | use.setAttribute('xlink:href', href); 60 | } else { 61 | use.setAttribute('href', href); 62 | } 63 | 64 | // only insert SVG content if it hasn't already been retrieved 65 | if (!requestCache[url]) { 66 | requestCache[url] = fetchSvg(url); 67 | } 68 | 69 | requestCache[url].then((svgContent) => { 70 | // create a document fragment from the svgContent returned (is parsed by HTML parser) 71 | if (!svgFragments[url]) { 72 | const svgFragment = document 73 | .createRange() 74 | .createContextualFragment(svgContent); 75 | 76 | svgFragments[url] = svgFragment; 77 | } 78 | if (!symbolEls[href]) { 79 | const svgFragment = svgFragments[url]; 80 | const symbolEl = svgFragment.querySelector( 81 | `#${id}` 82 | ); 83 | 84 | symbolEls[href] = true; 85 | symbolEl.id = `${namespace}-${id}`; 86 | spritesEl.appendChild(symbolEl); 87 | } 88 | }); 89 | } 90 | } 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/client/modules/datatools/dataviewer/dataviewer.html: -------------------------------------------------------------------------------- 1 | 92 | -------------------------------------------------------------------------------- /src/client/modules/datatools/fieldoptions/fieldoptions.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, track, api } from 'lwc'; 2 | 3 | export default class FieldOptions extends LightningElement { 4 | @api field; 5 | @track updatedField; 6 | @track updatedName; 7 | @track updateType; 8 | @track isEdit = false; 9 | @track hasChange = false; 10 | 11 | types = [ 12 | { value: 'Text', label: 'Text' }, 13 | { value: 'Date', label: 'Date' }, 14 | { value: 'Number', label: 'Number' }, 15 | { value: 'Decimal', label: 'Decimal' }, 16 | { value: 'Boolean', label: 'Boolean' }, 17 | { value: 'Phone', label: 'Phone' }, 18 | { value: 'Locale', label: 'Locale' }, 19 | { value: 'EmailAddress', label: 'Email Address' } 20 | ]; 21 | 22 | check; 23 | 24 | handleCheckboxChange(e) { 25 | this.updatedField.IsPrimaryKey = e.detail.value.includes( 26 | 'IsPrimaryKey' 27 | ); 28 | this.updatedField.IsRequired = e.detail.value.includes('IsRequired'); 29 | } 30 | 31 | get checkBoxValues() { 32 | const checked = []; 33 | if (this.updatedField.IsPrimaryKey) { 34 | checked.push('IsPrimaryKey'); 35 | } 36 | if (this.updatedField.IsRequired) { 37 | checked.push('IsRequired'); 38 | } 39 | return checked; 40 | } 41 | 42 | get checkboxOptions() { 43 | return [ 44 | { 45 | label: 'Primary Key', 46 | value: 'IsPrimaryKey' 47 | }, 48 | { 49 | label: 'Required', 50 | value: 'IsRequired' 51 | } 52 | ]; 53 | } 54 | 55 | get isText() { 56 | return this.updatedField.FieldType === 'Text'; 57 | } 58 | 59 | get isNumber() { 60 | return this.updatedField.FieldType === 'Number'; 61 | } 62 | 63 | get isDecimal() { 64 | return this.updatedField.FieldType === 'Decimal'; 65 | } 66 | 67 | connectedCallback() { 68 | this.updatedField = JSON.parse(JSON.stringify(this.field)); 69 | console.log(this.updatedField); 70 | } 71 | 72 | handleclick(e) { 73 | this.isEdit = true; 74 | console.log('click', e, this.field.Name, this.isEdit); 75 | } 76 | handlesave() { 77 | if (this.hasChange) { 78 | this.dispatchEvent( 79 | new CustomEvent('changefield', { 80 | bubbles: true, 81 | composed: true, 82 | detail: this.updatedField 83 | }) 84 | ); 85 | } 86 | this.isEdit = false; 87 | } 88 | handledelete() { 89 | this.dispatchEvent( 90 | new CustomEvent('deletefield', { 91 | bubbles: true, 92 | composed: true, 93 | detail: this.updatedField 94 | }) 95 | ); 96 | } 97 | 98 | handleclose() { 99 | this.updatedField = JSON.parse(JSON.stringify(this.field)); 100 | this.isEdit = false; 101 | } 102 | editname(e) { 103 | this.hasChange = true; 104 | this.updatedField.Name = e.detail.value; 105 | } 106 | editlen(e) { 107 | this.hasChange = true; 108 | this.updatedField.MaxLength = e.detail.value; 109 | } 110 | editprecision(e) { 111 | this.hasChange = true; 112 | this.updatedField.Precision = e.detail.value; 113 | } 114 | editscale(e) { 115 | this.hasChange = true; 116 | this.updatedField.Scale = e.detail.value; 117 | } 118 | edittype(e) { 119 | this.hasChange = true; 120 | //reset some values on type change 121 | if (e.detail.value !== 'Text') { 122 | delete this.updatedField.MaxLength; 123 | } else if (this.updatedField.MaxLength === null) { 124 | this.updatedField.MaxLength = 50; 125 | } 126 | if (e.detail.value !== 'Decimal') { 127 | delete this.updatedField.Precision; 128 | delete this.updatedField.Scale; 129 | } else if ( 130 | this.updatedField.Precision === null && 131 | this.updatedField.Scale === null 132 | ) { 133 | this.updatedField.Precision = 18; 134 | this.updatedField.Scale = 2; 135 | } 136 | this.updatedField.FieldType = e.detail.value; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/client/modules/datatools/dataassessor/dataassessor.html: -------------------------------------------------------------------------------- 1 | 101 | -------------------------------------------------------------------------------- /src/server/sfmc/data.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const core = require('./core.js'); 3 | /** @description returns all data extensions (up to 2500) 4 | * @memberof server/sfmc 5 | * @function 6 | * @param {Object} req express request object 7 | * @return {Array} name of Attribute Set if found 8 | */ 9 | exports.getDataExtensions = async (req) => { 10 | const body = { 11 | RetrieveRequestMsg: { 12 | RetrieveRequest: { 13 | ObjectType: 'DataExtension', 14 | Properties: ['Name', 'CustomerKey', 'CategoryID', 'ObjectID'] 15 | } 16 | } 17 | }; 18 | 19 | const response = await core.soapRequest(body, 'Retrieve', req.session.auth); 20 | return core.flattenResults(response.Results); 21 | }; 22 | /** @description returns all data extensions (up to 2500) 23 | * @memberof server/sfmc 24 | * @function 25 | * @param {Object} req request 26 | * @return {Array} name of Attribute Set if found 27 | */ 28 | exports.getDataExtensionFields = async (req) => { 29 | const fieldProperties = req.body.fields || ['Name', 'FieldType']; 30 | const body = { 31 | RetrieveRequestMsg: { 32 | RetrieveRequest: { 33 | ObjectType: 'DataExtensionField', 34 | //QueryAllAccounts: "true", 35 | Properties: fieldProperties, 36 | Filter: { 37 | $: { 38 | 'xsi:type': 'SimpleFilterPart' 39 | }, 40 | Property: 'DataExtension.CustomerKey', 41 | SimpleOperator: 'equals', 42 | Value: req.params.key 43 | } 44 | } 45 | } 46 | }; 47 | const rawResults = await core.soapRequest( 48 | body, 49 | 'Retrieve', 50 | req.session.auth 51 | ); 52 | return core.parseSOAPResponse(rawResults).map((field) => { 53 | const obj = {}; 54 | for (const key in field) { 55 | if (fieldProperties.includes(key)) { 56 | obj[key] = field[key][0]; 57 | } 58 | } 59 | return obj; 60 | }); 61 | }; 62 | /** @description returns all data extension rows (up to 2500) 63 | * @memberof server/sfmc 64 | * @function 65 | * @param {Object} req request 66 | * @return {Array} name of Attribute Set if found 67 | */ 68 | exports.getDataExtensionData = async (req, name, fields) => { 69 | const body = { 70 | RetrieveRequestMsg: { 71 | RetrieveRequest: { 72 | ObjectType: 'DataExtensionObject[' + name + ']', 73 | Properties: fields 74 | } 75 | } 76 | }; 77 | if (req.body.filter) { 78 | body.RetrieveRequestMsg.RetrieveRequest.Filter = { 79 | $: { 80 | 'xsi:type': 'SimpleFilterPart' 81 | }, 82 | Property: req.body.filter.field, 83 | SimpleOperator: req.body.filter.operator, 84 | Value: req.body.filter.value 85 | }; 86 | } 87 | const rawResults = await core.soapRequest( 88 | body, 89 | 'Retrieve', 90 | req.session.auth 91 | ); 92 | const parsedresults = core.parseSOAPResponse(rawResults); 93 | // convert key value array to object 94 | if (parsedresults) { 95 | const rows = parsedresults.map((row) => { 96 | const obj = {}; 97 | row.Properties[0].Property.filter((prop) => 98 | fields.includes(prop.Name[0]) 99 | ).forEach((prop) => { 100 | obj[prop.Name[0]] = prop.Value[0]; 101 | }); 102 | return obj; 103 | }); 104 | return rows; 105 | } 106 | return null; 107 | }; 108 | /** @description creates data extension 109 | * @memberof server/sfmc 110 | * @function 111 | * @param {Object} req request 112 | * @return {Array} name of Attribute Set if found 113 | */ 114 | exports.createDataExtension = async (req) => { 115 | const metadata = JSON.parse(req.body); 116 | const body = { 117 | CreateRequest: { 118 | Objects: { 119 | $: { 120 | 'xsi:type': 'DataExtension' 121 | }, 122 | Name: metadata.name, 123 | CustomerKey: metadata.name, 124 | Fields: { 125 | Field: metadata.fields 126 | } 127 | } 128 | } 129 | }; 130 | 131 | const rawResults = await core.soapRequest(body, 'Create', req.session.auth); 132 | const res = await core.parseSOAPResponse(rawResults); 133 | 134 | return res; 135 | }; 136 | -------------------------------------------------------------------------------- /src/client/modules/salesforcenotification/activity/activity.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/server/api.js: -------------------------------------------------------------------------------- 1 | // Simple Express server setup to serve for local testing/dev API server 2 | if (process.env.NODE_ENV !== 'production') { 3 | require('dotenv').config(); 4 | } 5 | const logger = require('./utils/logger'); 6 | const compression = require('compression'); 7 | const helmet = require('helmet'); 8 | const express = require('express'); 9 | const session = require('express-session'); 10 | const bodyParser = require('body-parser'); 11 | const rateLimit = require('express-rate-limit'); 12 | const RedisRateLimit = require('rate-limit-redis'); 13 | const Redis = require('ioredis'); 14 | 15 | const redisClient = new Redis(process.env.REDIS_URL); 16 | 17 | // static vars 18 | const DIST_DIR = './dist'; 19 | 20 | const app = express(); 21 | //add logging for all requests in debug mode 22 | app.use( 23 | require('morgan')('tiny', { 24 | stream: { write: (message) => logger.http(message.trim()) } 25 | }) 26 | ); 27 | 28 | // add iframe protections, except frameguard which causes issues being rendered in iframe of SFMC 29 | app.use( 30 | helmet({ 31 | frameguard: false 32 | }) 33 | ); 34 | app.use( 35 | helmet.contentSecurityPolicy({ 36 | directives: { 37 | defaultSrc: ["'self'", '*.exacttarget.com'], 38 | scriptSrc: ["'self'", '*.exacttarget.com'], 39 | objectSrc: ["'none'"], 40 | imgSrc: ["'self'", '*.exacttarget.com', "'unsafe-inline'"], 41 | styleSrc: ["'self'", "'unsafe-inline'"], 42 | upgradeInsecureRequests: [] 43 | } 44 | }) 45 | ); 46 | app.use(compression()); 47 | 48 | app.use(bodyParser.urlencoded({ extended: true })); 49 | app.use(bodyParser.text({ type: 'text/plain', limit: '10mb' })); 50 | app.use(bodyParser.json()); 51 | app.use(bodyParser.raw({ type: 'application/jwt' })); 52 | 53 | // used for holding session store over restarts 54 | let RedisStore = require('connect-redis')(session); 55 | app.set('trust proxy', 1); 56 | app.use( 57 | session({ 58 | store: new RedisStore({ client: redisClient }), 59 | secret: process.env.SECRET_TOKEN, 60 | cookie: { 61 | secure: true, 62 | maxAge: 24 * 60 * 60 * 1000, // 24 hours 63 | sameSite: 'none', 64 | httpOnly: true 65 | }, 66 | resave: false, 67 | saveUninitialized: false 68 | }) 69 | ); 70 | // Rate Limit API requests 71 | // we exclude routes ending with execute since these may be used 72 | // thousands of times be Journey Builder in short period 73 | app.use( 74 | /.*[^execute]$/, 75 | rateLimit({ 76 | store: new RedisRateLimit({ 77 | client: redisClient 78 | }), 79 | windowMs: 15 * 60 * 1000, // 15 minutes 80 | max: 100 81 | }) 82 | ); 83 | 84 | app.get('/session', (req, res) => { 85 | res.json({ success: true, session: req.session }); 86 | }); 87 | 88 | //generic SFMC endpoint which has some helpful things in 89 | app.use('/sfmc', require('./sfmc/sfmc-api.js')); 90 | 91 | //put your custom endpoints here 92 | app.use('/dataTools', require('./dataTools/dataTools-api.js')); 93 | // this is a common app for anything connecting to SF 94 | app.use( 95 | '/salesforceconfig', 96 | require('./salesforceconfig/salesforceconfig-api.js') 97 | ); 98 | app.use('/platformevent', require('./platformevent/platformevent-api.js')); 99 | app.use( 100 | '/salesforcenotification', 101 | require('./salesforcenotification/salesforcenotification-api.js') 102 | ); 103 | app.use(express.static(DIST_DIR)); 104 | 105 | if (process.env.NODE_ENV !== 'production') { 106 | //local version 107 | const https = require('https'); 108 | const path = require('path'); 109 | const fs = require('fs'); 110 | 111 | https 112 | .createServer( 113 | { 114 | key: fs.readFileSync( 115 | path.join( 116 | __dirname, 117 | '..', 118 | '..', 119 | 'certificates', 120 | 'private.key' 121 | ), 122 | 'ascii' 123 | ), 124 | cert: fs.readFileSync( 125 | path.join( 126 | __dirname, 127 | '..', 128 | '..', 129 | 'certificates', 130 | 'private.crt' 131 | ), 132 | 'ascii' 133 | ) 134 | }, 135 | app 136 | ) 137 | .listen(process.env.PORT, () => { 138 | logger.info( 139 | `✅ Test Server started: https://127.0.0.1:${process.env.PORT}/` 140 | ); 141 | }); 142 | } else { 143 | //production build 144 | app.listen(process.env.PORT, () => 145 | logger.info( 146 | `✅ Production Server started: http(s)://${process.env.HEROKU_APP_NAME}.herokuapp.com:${process.env.PORT}/` 147 | ) 148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/server/platformevent/platformevent-api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('../utils/logger'); 3 | const sfdc = require('../sfdc/index.js'); 4 | const router = express.Router({ strict: true }); 5 | const csurf = require('csurf')(); 6 | const { checkAuth, getRedirectURL } = require('../sfmc/core.js'); 7 | const { decode } = require('../utils/jwt'); 8 | 9 | //default entry path with auth validation 10 | router.get('/activity', csurf, (req, res, next) => { 11 | res.cookie('XSRF-TOKEN', req.csrfToken(), { 12 | sameSite: 'none', 13 | secure: true 14 | }); 15 | checkAuth(req, res, next, req.originalUrl.substring(1)); 16 | }); 17 | 18 | // path in case we want to force a refresh of token 19 | router.get('/activity/login', csurf, (req, res) => { 20 | res.redirect( 21 | 301, 22 | getRedirectURL(req, req.originalUrl.replace('/login', '').substring(1)) 23 | ); 24 | }); 25 | 26 | router.get('/config.json', (req, res) => { 27 | const config = { 28 | workflowApiVersion: '1.1', 29 | metaData: { 30 | // the location of our icon file 31 | icon: `https://${req.headers.host}/assets/platformeventsicon.png`, 32 | category: 'customer', 33 | backgroundColor: '#032e61', 34 | expressionBuilderPrefix: 'Platform' 35 | }, 36 | // allows copying of activity (undocumented) 37 | copySettings: { 38 | allowCopy: true, 39 | displayCopyNotification: true 40 | }, 41 | // For Custom Activity this must say, "REST" 42 | type: 'REST', 43 | lang: { 44 | 'en-US': { 45 | name: 'Platform Event', 46 | description: 'Use for sending a platform Event' 47 | } 48 | }, 49 | arguments: { 50 | execute: { 51 | // See: https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/how-data-binding-works.htm 52 | inArguments: [], 53 | outArguments: [], 54 | // Fill in the host with the host that this is running on. 55 | // It must run under HTTPS 56 | url: `https://${req.headers.host}/platformevent/execute`, 57 | // The amount of time we want Journey Builder to wait before cancel the request. Default is 60000, Minimal is 1000 58 | timeout: 10000, 59 | // how many retrys if the request failed with 5xx error or network error. default is 0 60 | retryCount: 3, 61 | // wait in ms between retry. 62 | retryDelay: 1000, 63 | // The number of concurrent requests Journey Builder will send all together 64 | concurrentRequests: 5, 65 | // sign request 66 | useJwt: true 67 | } 68 | }, 69 | configurationArguments: { 70 | publish: { 71 | url: `https://${req.headers.host}/platformevent/publish`, 72 | useJwt: true 73 | }, 74 | validate: { 75 | url: `https://${req.headers.host}/platformevent/validate`, 76 | useJwt: true 77 | }, 78 | stop: { 79 | url: `https://${req.headers.host}/platformevent/stop`, 80 | useJwt: true 81 | }, 82 | save: { 83 | url: `https://${req.headers.host}/platformevent/save`, 84 | useJwt: true 85 | } 86 | }, 87 | userInterfaces: { 88 | configurationSupportsReadOnlyMode: true, 89 | configInspector: { 90 | size: 'scm-lg', 91 | emptyIframe: true 92 | } 93 | }, 94 | schema: { 95 | arguments: { 96 | execute: { 97 | inArguments: [], 98 | outArguments: [] 99 | } 100 | } 101 | }, 102 | edit: { 103 | url: `https://${req.headers.host}/platformevent/activity` 104 | } 105 | }; 106 | res.json(config); 107 | }); 108 | 109 | router.post('/execute', decode, (req, res) => { 110 | logger.info('execute request:', req.body); 111 | sfdc.publishEvent( 112 | req.body.inArguments[0].event, 113 | req.body.inArguments[1].fields, 114 | req.body.inArguments[2].mid 115 | ); 116 | 117 | res.json({ status: 'ok' }); 118 | }); 119 | router.post('/publish', decode, (req, res) => { 120 | logger.debug('publish request', req.body); 121 | res.json({ status: 'ok' }); 122 | }); 123 | router.post('/stop', decode, (req, res) => { 124 | logger.debug('stop request', req.body); 125 | res.json({ status: 'ok' }); 126 | }); 127 | router.post('/validate', decode, (req, res) => { 128 | logger.debug('validate request', req.body); 129 | res.json({ status: 'ok' }); 130 | }); 131 | router.post('/save', decode, (req, res) => { 132 | logger.debug('save request', req.body); 133 | res.json({ status: 'ok' }); 134 | }); 135 | 136 | router.get('/platformEvents', checkAuth, async (req, res) => { 137 | //logger.info(core.checkAuth); 138 | try { 139 | const platformEvents = await sfdc.getMetadata( 140 | req.session.context.organization.member_id 141 | ); 142 | res.json(platformEvents); 143 | } catch (ex) { 144 | res.status(500).json({ message: ex.message }); 145 | } 146 | }); 147 | router.get('/context', (req, res) => { 148 | res.json(req.session.context); 149 | }); 150 | 151 | module.exports = router; 152 | -------------------------------------------------------------------------------- /src/client/modules/common/activity/activity.js: -------------------------------------------------------------------------------- 1 | import { LightningElement, api, track } from 'lwc'; 2 | import Postmonger from 'postmonger'; 3 | const connection = new Postmonger.Session(); 4 | import setupTestHarness from './testHarness'; 5 | 6 | export default class App extends LightningElement { 7 | @api nextValue = 'Done'; 8 | @api prevValue = 'Cancel'; 9 | @api canNext; 10 | @track canSave = false; 11 | @api title; 12 | @api icon; 13 | @api events; 14 | @api context; 15 | @track state = {}; 16 | 17 | availableEvents = [ 18 | 'tokens', 19 | 'culture', 20 | 'endpoints', 21 | 'schema', 22 | 'interaction', 23 | 'triggerEventDefinition', 24 | //unsupported - these exist, but not sure what it does - found in https://jbinteractions.s7.marketingcloudapps.com/canvas/js/customIframeBaseView.js 25 | 'dataSource', 26 | //'allowedOriginResponse', 27 | 'interactionGoalStats', 28 | 'activityPermissions', 29 | 'engineSettings', 30 | 'dataLibrarySource', 31 | 'contactsSchema', 32 | 'expressionBuilderAttributes', 33 | 'interactionDefaults', 34 | 'userTimeZone', 35 | 'entryEventDefinitionKey', 36 | 'i18nConfig', 37 | 'activityPayload', 38 | 'dataSources' 39 | ]; 40 | availableContexts = [ 41 | 'activity', 42 | 'initActivityRunningHover', 43 | 'initActivityRunningModal' 44 | ]; 45 | 46 | connectedCallback() { 47 | //run this first, will not do anything if not running locally 48 | setupTestHarness(connection); 49 | for (const e of this.events) { 50 | // 51 | if (this.availableEvents.includes(e)) { 52 | const t = e.charAt(0).toUpperCase() + e.slice(1); 53 | 54 | connection.on('requested' + t, (r) => { 55 | //if only one key and same as event then skip the added name 56 | if ( 57 | typeof r === 'object' && 58 | r !== null && 59 | Object.keys(r).length === 0 && 60 | Object.keys(r)[0] === e 61 | ) { 62 | this.setState(e, r); 63 | } else { 64 | const obj = {}; 65 | obj[e] = r; 66 | this.setState(e, obj); 67 | } 68 | }); 69 | if (e === 'interactionGoalStats') { 70 | connection.trigger('request' + t, true); 71 | } else if (e === 'activityPermissions') { 72 | connection.trigger('request' + t, null); 73 | } else if (e === 'dataLibrarySource') { 74 | connection.trigger('request' + t, 'react'); 75 | } else { 76 | connection.trigger('request' + t); 77 | } 78 | } 79 | } 80 | if (this.availableContexts.includes(this.context)) { 81 | const t = 82 | this.context.charAt(0).toUpperCase() + this.context.slice(1); 83 | connection.on('init' + t, (payload) => { 84 | this.setState('payload', { payload }); 85 | }); 86 | connection.trigger('ready'); 87 | } else { 88 | throw new Error('Unsupported Context:' + this.context); 89 | } 90 | } 91 | 92 | setState(eventName, obj) { 93 | console.log('[setState]', eventName, obj); 94 | this.state = Object.assign(this.state || {}, obj); 95 | if ( 96 | this.events.filter((e) => !Object.keys(this.state).includes(e)) 97 | .length === 0 && 98 | this.state.payload != null 99 | ) { 100 | this.dispatchEvent( 101 | new CustomEvent('context', { 102 | bubbles: true, 103 | composed: true, 104 | detail: this.state 105 | }) 106 | ); 107 | } 108 | } 109 | 110 | // used to change size of modal 111 | @api resize(size) { 112 | // TBC what the sizes are 113 | console.log('[resize]', size); 114 | connection.trigger('requestInspectorResize', size); 115 | } 116 | // let journey builder know the activity has changes so it asks if you click close 117 | @api hasChanges() { 118 | console.log('[hasChanges]'); 119 | 120 | connection.trigger('setActivityDirtyState', true); 121 | } 122 | // force close of activity 123 | cancel() { 124 | console.log('[cancel]'); 125 | // now request that Journey Builder closes the inspector/drawer 126 | connection.trigger('requestInspectorClose'); 127 | } 128 | // sync states between activity and framework 129 | @api update(payload) { 130 | console.log('[update]', payload); 131 | this.state.payload = payload; 132 | } 133 | // update journey builder with new config 134 | done() { 135 | console.log('[updateActivity]'); 136 | //todo: add better validation checks 137 | if (this.state.payload) { 138 | connection.trigger('updateActivity', this.state.payload); 139 | } else { 140 | this.dispatchEvent( 141 | new CustomEvent('error', { 142 | bubbles: true, 143 | composed: true, 144 | detail: { 145 | message: 'Payload to update was invalid' 146 | } 147 | }) 148 | ); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/client/modules/datatools/fieldoptions/fieldoptions.html: -------------------------------------------------------------------------------- 1 | 136 | -------------------------------------------------------------------------------- /src/server/sfdc/index.js: -------------------------------------------------------------------------------- 1 | const logger = require('../utils/logger'); 2 | const jsforce = require('jsforce'); 3 | const Redis = require('ioredis'); 4 | 5 | const redis = new Redis(process.env.REDIS_URL, { keyPrefix: 'sfdc:' }); 6 | const connectionArray = {}; 7 | const SF_API_VERSION = '51.0'; 8 | 9 | exports.getMetadata = async (mid) => { 10 | const res = await connectionArray[mid].describeGlobal(); 11 | return Promise.all( 12 | res.sobjects 13 | .filter((obj) => obj.name.endsWith('__e')) 14 | .map((obj) => connectionArray[mid].describe(obj.name)) 15 | ); 16 | }; 17 | 18 | exports.publishEvent = async (event, fields, mid) => { 19 | const createRes = await connectionArray[mid].sobject(event).create(fields); 20 | logger.info('createRes', createRes); 21 | return createRes; 22 | }; 23 | 24 | const init = async () => { 25 | const connKeys = await redis.keys('sfdc:*'); 26 | connKeys.forEach(async (key) => { 27 | const withoutPrefix = key.replace('sfdc:', ''); 28 | const conn = await redis.get(withoutPrefix); 29 | 30 | connectionArray[withoutPrefix] = new jsforce.Connection( 31 | JSON.parse(conn) 32 | ); 33 | // add refresh handler to save refresh token 34 | connectionArray[withoutPrefix].on('refresh', (accessToken, res) => { 35 | logger.info('on Refresh', accessToken, res); 36 | saveCredentials(withoutPrefix, connectionArray[withoutPrefix]); 37 | logger.info('refreshed and saved credentials'); 38 | // Refresh event will be fired when renewed access token 39 | // to store it in your storage for next request 40 | }); 41 | }); 42 | }; 43 | 44 | exports.loginurl = (cred, hostname, mid, state) => { 45 | connectionArray[mid] = new jsforce.Connection({ 46 | version: SF_API_VERSION, 47 | loginUrl: cred.sfdcurl, 48 | oauth2: { 49 | loginUrl: cred.sfdcurl, 50 | clientId: cred.sfdcclientid, 51 | clientSecret: cred.sfdcclientsecret, 52 | redirectUri: 53 | `https://${hostname}/salesforceconfig/oauth/response/` + mid 54 | } 55 | }); 56 | return connectionArray[mid].oauth2.getAuthorizationUrl({ 57 | scope: 'api id web refresh_token offline_access', 58 | state: state 59 | }); 60 | }; 61 | 62 | exports.status = async (mid) => { 63 | if (connectionArray[mid]) { 64 | if (connectionArray[mid].oauth2.clientId) { 65 | let user = await connectionArray[mid].identity(); 66 | return { 67 | organization_id: user.organization_id, 68 | username: user.username, 69 | clientId: connectionArray[mid].oauth2.clientId, 70 | loginUrl: connectionArray[mid].instanceUrl 71 | }; 72 | } 73 | return null; 74 | } 75 | return { mid: mid, status: 'not found' }; 76 | }; 77 | 78 | exports.authorize = async (mid, code) => { 79 | logger.info('authorizing for mid', mid); 80 | try { 81 | const userInfo = await connectionArray[mid].authorize(code); 82 | logger.info('userInfo', userInfo); 83 | connectionArray[mid] = new jsforce.Connection(connectionArray[mid]); 84 | connectionArray[mid].on('refresh', async (accessToken, res) => { 85 | logger.info('on Refresh', accessToken, res); 86 | await saveCredentials(mid, connectionArray[mid]); 87 | logger.info('refreshed and saved credentials'); 88 | // Refresh event will be fired when renewed access token 89 | // to store it in your storage for next request 90 | }); 91 | 92 | await saveCredentials(mid, connectionArray[mid]); 93 | // Now you can get the access token, refresh token, and instance URL information. 94 | // Save them to establish connection next time. 95 | logger.info('User ID: ' + userInfo.id); 96 | logger.info('Org ID: ' + userInfo.organizationId); 97 | // ... 98 | return userInfo; 99 | } catch (err) { 100 | logger.error('ERROR authorize:', err); 101 | throw err; 102 | } 103 | }; 104 | 105 | async function saveCredentials(mid, conf) { 106 | logger.info('saving credentials to redis', mid, { 107 | version: conf.version, 108 | oauth2: conf.oauth2, 109 | accessToken: conf.accessToken, 110 | refreshToken: conf.refreshToken, 111 | instanceUrl: conf.instanceUrl 112 | }); 113 | 114 | const setCred = await redis.set( 115 | mid, 116 | JSON.stringify({ 117 | version: SF_API_VERSION, 118 | oauth2: conf.oauth2, 119 | accessToken: conf.accessToken, 120 | refreshToken: conf.refreshToken, 121 | instanceUrl: conf.instanceUrl 122 | }) 123 | ); 124 | logger.info('Credentials Set', setCred); 125 | } 126 | 127 | exports.getNotificationTypes = async (mid) => { 128 | return connectionArray[mid].tooling.query( 129 | 'Select Id,CustomNotifTypeName from CustomNotificationType' 130 | ); 131 | }; 132 | 133 | exports.publishNotification = async ( 134 | typeId, 135 | content, 136 | recipientId, 137 | targetId, 138 | mid 139 | ) => { 140 | const payload = [ 141 | { 142 | customNotifTypeId: typeId, 143 | recipientIds: [recipientId], 144 | title: 'Marketing Cloud Notification', 145 | body: content, 146 | targetId: targetId 147 | } 148 | ]; 149 | console.log(payload); 150 | const res = await connectionArray[mid].requestPost( 151 | '/actions/standard/customNotificationAction', 152 | { inputs: payload } 153 | ); 154 | logger.info('publishNotification', res); 155 | return res; 156 | }; 157 | 158 | init(); 159 | -------------------------------------------------------------------------------- /src/server/salesforcenotification/salesforcenotification-api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require('../utils/logger'); 3 | const sfdc = require('../sfdc/index.js'); 4 | const router = express.Router({ strict: true }); 5 | const csurf = require('csurf')(); 6 | const { checkAuth, getRedirectURL } = require('../sfmc/core.js'); 7 | const { decode } = require('../utils/jwt'); 8 | 9 | //default entry path with auth validation 10 | router.get(['/activity'], csurf, (req, res, next) => { 11 | res.cookie('XSRF-TOKEN', req.csrfToken(), { 12 | sameSite: 'none', 13 | secure: true 14 | }); 15 | checkAuth(req, res, next, req.originalUrl.substring(1)); 16 | }); 17 | 18 | // path in case we want to force a refresh of token 19 | router.get(['/activity/login'], csurf, (req, res) => { 20 | res.redirect( 21 | 301, 22 | getRedirectURL(req, req.originalUrl.replace('/login', '').substring(1)) 23 | ); 24 | }); 25 | 26 | router.get('/config.json', (req, res) => { 27 | const config = { 28 | workflowApiVersion: '1.1', 29 | metaData: { 30 | // the location of our icon file 31 | icon: `https://${req.headers.host}/assets/notification.png`, 32 | category: 'messaging', 33 | backgroundColor: '#032e61', 34 | expressionBuilderPrefix: 'sfnotif' 35 | }, 36 | // allows copying of activity (undocumented) 37 | copySettings: { 38 | allowCopy: true, 39 | displayCopyNotification: true 40 | }, 41 | // For Custom Activity this must say, "REST" 42 | type: 'REST', 43 | lang: { 44 | 'en-US': { 45 | name: 'Salesforce Notification', 46 | description: 47 | 'Send a Salesforce Notification to a User or Contact' 48 | } 49 | }, 50 | arguments: { 51 | execute: { 52 | // See: https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/how-data-binding-works.htm 53 | inArguments: [], 54 | outArguments: [], 55 | // Fill in the host with the host that this is running on. 56 | // It must run under HTTPS 57 | url: `https://${req.headers.host}/salesforcenotification/execute`, 58 | // The amount of time we want Journey Builder to wait before cancel the request. Default is 60000, Minimal is 1000 59 | timeout: 10000, 60 | // how many retrys if the request failed with 5xx error or network error. default is 0 61 | retryCount: 3, 62 | // wait in ms between retry. 63 | retryDelay: 1000, 64 | // The number of concurrent requests Journey Builder will send all together 65 | concurrentRequests: 5, 66 | // sign request 67 | useJwt: true 68 | } 69 | }, 70 | configurationArguments: { 71 | publish: { 72 | url: `https://${req.headers.host}/salesforcenotification/publish`, 73 | useJwt: true 74 | }, 75 | validate: { 76 | url: `https://${req.headers.host}/salesforcenotification/validate`, 77 | useJwt: true 78 | }, 79 | stop: { 80 | url: `https://${req.headers.host}/salesforcenotification/stop`, 81 | useJwt: true 82 | }, 83 | save: { 84 | url: `https://${req.headers.host}/salesforcenotification/save`, 85 | useJwt: true 86 | } 87 | }, 88 | userInterfaces: { 89 | configurationSupportsReadOnlyMode: true, 90 | configInspector: { 91 | size: 'scm-lg', 92 | emptyIframe: true 93 | } 94 | }, 95 | schema: { 96 | arguments: { 97 | execute: { 98 | inArguments: [], 99 | outArguments: [] 100 | } 101 | } 102 | }, 103 | edit: { 104 | url: `https://${req.headers.host}/salesforcenotification/activity` 105 | } 106 | }; 107 | res.json(config); 108 | }); 109 | 110 | router.post('/execute', decode, (req, res) => { 111 | logger.info('execute request:', req.body); 112 | sfdc.publishNotification( 113 | req.body.inArguments[0].type, 114 | req.body.inArguments[0].content, 115 | req.body.inArguments[0].recipient, 116 | req.body.inArguments[0].target, 117 | req.body.inArguments[0].mid 118 | ); 119 | 120 | res.json({ status: 'ok' }); 121 | }); 122 | router.post('/publish', decode, (req, res) => { 123 | logger.debug('publish request', req.body); 124 | res.json({ status: 'ok' }); 125 | }); 126 | router.post('/stop', decode, (req, res) => { 127 | logger.debug('stop request', req.body); 128 | res.json({ status: 'ok' }); 129 | }); 130 | router.post('/validate', decode, (req, res) => { 131 | logger.debug('validate request', req.body); 132 | res.json({ status: 'ok' }); 133 | }); 134 | router.post('/save', decode, (req, res) => { 135 | logger.debug('save request', req.body); 136 | res.json({ status: 'ok' }); 137 | }); 138 | 139 | router.get('/notificationTypes', checkAuth, async (req, res) => { 140 | //logger.info(core.checkAuth); 141 | try { 142 | const notificationTypes = await sfdc.getNotificationTypes( 143 | req.session.context.organization.member_id 144 | ); 145 | res.json(notificationTypes); 146 | } catch (ex) { 147 | res.status(500).json({ message: ex.message }); 148 | } 149 | }); 150 | router.get('/context', (req, res) => { 151 | res.json(req.session.context); 152 | }); 153 | 154 | module.exports = router; 155 | --------------------------------------------------------------------------------