├── .forceignore ├── .gitignore ├── .sfdx ├── orgs │ ├── test-dnx9msp7kmb3@example.com │ │ ├── metadataTypeInfos.json │ │ ├── sourcePathInfos.json │ │ └── sourcePathInfos.json.bak │ ├── test-fxt7aqluvxan@example.com │ │ ├── maxrevision.json │ │ ├── metadataTypeInfos.json │ │ ├── sourcePathInfos.json │ │ └── sourcePathInfos.json.bak │ ├── test-ixnjmgxaf3ap@example.com │ │ ├── maxrevision.json │ │ ├── metadataTypeInfos.json │ │ └── sourcePathInfos.json │ ├── test-sbu1u52ah4xc@example.com │ │ ├── maxrevision.json │ │ ├── metadataTypeInfos.json │ │ └── sourcePathInfos.json │ └── test-ylacu6ikceqn@example.com │ │ ├── maxrevision.json │ │ ├── metadataTypeInfos.json │ │ └── sourcePathInfos.json ├── sfdx-config.json ├── tools │ └── apex.db └── typings │ └── lwc │ ├── apex.d.ts │ ├── apex │ ├── ExpressionBuilder.d.ts │ ├── FieldPickerController.d.ts │ ├── FormulaBuilderController.d.ts │ └── SearchUtils.d.ts │ ├── customlabels.d.ts │ ├── engine.d.ts │ ├── lds.d.ts │ ├── schema.d.ts │ ├── staticresources.d.ts │ └── user.d.ts ├── .vscode └── settings.json ├── Calculation__c.json ├── README.md ├── config └── project-scratch-def.json ├── force-app ├── .DS_Store └── main │ ├── .DS_Store │ └── default │ ├── .DS_Store │ ├── classes │ ├── AssembleInputParams.cls │ ├── AssembleInputParams.cls-meta.xml │ ├── EvaluateFormula.cls │ ├── EvaluateFormula.cls-meta.xml │ ├── ExpressionBuilder.cls │ ├── ExpressionBuilder.cls-meta.xml │ ├── ExpressionBuilderTest.cls │ ├── ExpressionBuilderTest.cls-meta.xml │ ├── FieldPickerController.cls │ ├── FieldPickerController.cls-meta.xml │ ├── FieldPickerControllerTest.cls │ ├── FieldPickerControllerTest.cls-meta.xml │ ├── FormulaBuilderController.cls │ ├── FormulaBuilderController.cls-meta.xml │ ├── FormulaBuilderControllerTest.cls │ ├── FormulaBuilderControllerTest.cls-meta.xml │ ├── FormulaEvaluator.cls │ ├── FormulaEvaluator.cls-meta.xml │ ├── FormulaEvaluatorTest.cls │ ├── FormulaEvaluatorTest.cls-meta.xml │ ├── RegExps.cls │ ├── RegExps.cls-meta.xml │ ├── SearchUtils.cls │ ├── SearchUtils.cls-meta.xml │ ├── SearchUtilsTest.cls │ ├── SearchUtilsTest.cls-meta.xml │ ├── UpdateField.cls │ ├── UpdateField.cls-meta.xml │ ├── UpdateFieldTest.cls │ └── UpdateFieldTest.cls-meta.xml │ ├── flows │ └── TestFlow_EvaluateFormula.flow-meta.xml │ ├── labels │ └── CustomLabels.labels-meta.xml │ ├── lwc │ ├── .eslintrc.json │ ├── buttonUtils │ │ ├── buttonUtils.js │ │ └── buttonUtils.js-meta.xml │ ├── expressionBuilder │ │ ├── expressionBuilder.html │ │ ├── expressionBuilder.js │ │ └── expressionBuilder.js-meta.xml │ ├── expressionLine │ │ ├── expressionLine.html │ │ ├── expressionLine.js │ │ └── expressionLine.js-meta.xml │ ├── formulaBuilder │ │ ├── formulaBuilder.css │ │ ├── formulaBuilder.html │ │ ├── formulaBuilder.js │ │ └── formulaBuilder.js-meta.xml │ ├── jsconfig.json │ ├── lwcLogger │ │ ├── lwcLogger.js │ │ └── lwcLogger.js-meta.xml │ ├── pickObjectAndFieldFSC │ │ ├── pickObjectAndFieldFSC.css │ │ ├── pickObjectAndFieldFSC.html │ │ ├── pickObjectAndFieldFSC.js │ │ └── pickObjectAndFieldFSC.js-meta.xml │ ├── setOwner │ │ ├── setOwner.css │ │ ├── setOwner.html │ │ ├── setOwner.js │ │ └── setOwner.js-meta.xml │ ├── setPickList │ │ ├── setPickList.html │ │ ├── setPickList.js │ │ └── setPickList.js-meta.xml │ └── updateFieldCPE │ │ ├── updateFieldCPE.html │ │ ├── updateFieldCPE.js │ │ └── updateFieldCPE.js-meta.xml │ └── staticresources │ ├── SiteSamples.resource-meta.xml │ └── SiteSamples │ ├── SiteStyles.css │ └── img │ ├── clock.png │ ├── construction.png │ ├── force_logo.png │ ├── maintenance.png │ ├── poweredby.png │ ├── tools.png │ ├── unauthorized.png │ └── warning.png ├── mdapi ├── classes │ ├── AssembleInputParams.cls │ ├── AssembleInputParams.cls-meta.xml │ ├── EvaluateFormula.cls │ ├── EvaluateFormula.cls-meta.xml │ ├── ExpressionBuilder.cls │ ├── ExpressionBuilder.cls-meta.xml │ ├── ExpressionBuilderTest.cls │ ├── ExpressionBuilderTest.cls-meta.xml │ ├── FieldPickerController.cls │ ├── FieldPickerController.cls-meta.xml │ ├── FieldPickerControllerTest.cls │ ├── FieldPickerControllerTest.cls-meta.xml │ ├── FormulaBuilderController.cls │ ├── FormulaBuilderController.cls-meta.xml │ ├── FormulaBuilderControllerTest.cls │ ├── FormulaBuilderControllerTest.cls-meta.xml │ ├── FormulaEvaluator.cls │ ├── FormulaEvaluator.cls-meta.xml │ ├── FormulaEvaluatorTest.cls │ ├── FormulaEvaluatorTest.cls-meta.xml │ ├── RegExps.cls │ ├── RegExps.cls-meta.xml │ ├── SearchUtils.cls │ ├── SearchUtils.cls-meta.xml │ ├── SearchUtilsTest.cls │ └── SearchUtilsTest.cls-meta.xml ├── flows │ └── TestFlow_EvaluateFormula.flow ├── labels │ └── CustomLabels.labels ├── lwc │ ├── buttonUtils │ │ ├── buttonUtils.js │ │ └── buttonUtils.js-meta.xml │ ├── expressionBuilder │ │ ├── expressionBuilder.html │ │ ├── expressionBuilder.js │ │ └── expressionBuilder.js-meta.xml │ ├── expressionLine │ │ ├── expressionLine.html │ │ ├── expressionLine.js │ │ └── expressionLine.js-meta.xml │ ├── formulaBuilder │ │ ├── formulaBuilder.css │ │ ├── formulaBuilder.html │ │ ├── formulaBuilder.js │ │ └── formulaBuilder.js-meta.xml │ ├── lwcLogger │ │ ├── lwcLogger.js │ │ └── lwcLogger.js-meta.xml │ ├── pickObjectAndFieldFSC │ │ ├── pickObjectAndFieldFSC.css │ │ ├── pickObjectAndFieldFSC.html │ │ ├── pickObjectAndFieldFSC.js │ │ └── pickObjectAndFieldFSC.js-meta.xml │ ├── setOwner │ │ ├── setOwner.css │ │ ├── setOwner.html │ │ ├── setOwner.js │ │ └── setOwner.js-meta.xml │ ├── setPickList │ │ ├── setPickList.html │ │ ├── setPickList.js │ │ └── setPickList.js-meta.xml │ └── updateFieldCPE │ │ ├── updateFieldCPE.html │ │ ├── updateFieldCPE.js │ │ └── updateFieldCPE.js-meta.xml ├── package.xml └── staticresources │ ├── SiteSamples.resource │ └── SiteSamples.resource-meta.xml └── sfdx-project.json /.forceignore: -------------------------------------------------------------------------------- 1 | **/jsconfig.json 2 | **profiles 3 | **objects 4 | **layouts 5 | **tabs 6 | force-app/main/default/classes/UpdateField* 7 | **/.eslintrc.json 8 | 9 | .DS_Store 10 | force-app/.DS_Store 11 | force-app/main/.DS_Store 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | force-app/.DS_Store 3 | force-app/main/.DS_Store 4 | /.idea/ 5 | /.sfdx/ 6 | /.vscode/ 7 | /config/ 8 | /IlluminatedCloud/ 9 | force-app/main/default/profiles 10 | -------------------------------------------------------------------------------- /.sfdx/orgs/test-fxt7aqluvxan@example.com/maxrevision.json: -------------------------------------------------------------------------------- 1 | 187 -------------------------------------------------------------------------------- /.sfdx/orgs/test-ixnjmgxaf3ap@example.com/maxrevision.json: -------------------------------------------------------------------------------- 1 | 204 -------------------------------------------------------------------------------- /.sfdx/orgs/test-sbu1u52ah4xc@example.com/maxrevision.json: -------------------------------------------------------------------------------- 1 | 213 -------------------------------------------------------------------------------- /.sfdx/orgs/test-ylacu6ikceqn@example.com/maxrevision.json: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /.sfdx/sfdx-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultdevhubusername": "taft@gs0.org", 3 | "defaultusername": "formula6S" 4 | } -------------------------------------------------------------------------------- /.sfdx/tools/apex.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/.sfdx/tools/apex.db -------------------------------------------------------------------------------- /.sfdx/typings/lwc/apex.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/apex" { 2 | /** 3 | * Identifier for an object's field. 4 | */ 5 | export interface FieldId { 6 | /** The field's API name. */ 7 | fieldApiName: string; 8 | /** The object's API name. */ 9 | objectApiName: string; 10 | } 11 | 12 | /** 13 | * Services for Apex. 14 | */ 15 | export interface ApexServices { 16 | /** 17 | * Refreshes a property annotated with @wire. Queries the server for updated data and refreshes the cache. 18 | * @param wiredTargetValue A property annotated with @wire. 19 | * @returns Promise that resolves to the refreshed value. If an error occurs, the promise is rejected. 20 | */ 21 | refreshApex: (wiredTargetValue: any) => Promise; 22 | 23 | /** 24 | * Gets a field value from an Apex sObject. 25 | * @param sObject The sObject holding the field. 26 | * @param field The field to return. 27 | * @returns The field's value. If it doesn't exist, undefined is returned. 28 | */ 29 | getSObjectValue: (sObject: object, field: string | FieldId) => any; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/apex/ExpressionBuilder.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/apex/ExpressionBuilder.assembleFormulaString" { 2 | export default function assembleFormulaString(param: {customLogic: any, logicType: any, expressionLines: any}): Promise; 3 | } 4 | declare module "@salesforce/apex/ExpressionBuilder.disassemblyFormulaString" { 5 | export default function disassemblyFormulaString(param: {expression: any}): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/apex/FieldPickerController.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/apex/FieldPickerController.getObjects" { 2 | export default function getObjects(param: {availableObjectTypes: any}): Promise; 3 | } 4 | declare module "@salesforce/apex/FieldPickerController.getPicklistValues" { 5 | export default function getPicklistValues(param: {objectApiName: any, fieldName: any}): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/apex/FormulaBuilderController.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/apex/FormulaBuilderController.getFieldList" { 2 | export default function getFieldList(param: {objectName: any}): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/apex/SearchUtils.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/apex/SearchUtils.searchMemberByType" { 2 | export default function searchMemberByType(param: {memberTypes: any, searchString: any}): Promise; 3 | } 4 | declare module "@salesforce/apex/SearchUtils.getSingleMembersByTypeAndId" { 5 | export default function getSingleMembersByTypeAndId(param: {type: any, id: any}): Promise; 6 | } 7 | declare module "@salesforce/apex/SearchUtils.describeSObjects" { 8 | export default function describeSObjects(param: {types: any}): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/customlabels.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/label/c.AccessLevel" { 2 | var AccessLevel: string; 3 | export default AccessLevel; 4 | } 5 | declare module "@salesforce/label/c.Back" { 6 | var Back: string; 7 | export default Back; 8 | } 9 | declare module "@salesforce/label/c.ButtonIsNotSupportedMessage" { 10 | var ButtonIsNotSupportedMessage: string; 11 | export default ButtonIsNotSupportedMessage; 12 | } 13 | declare module "@salesforce/label/c.ConditionLogicHelpText" { 14 | var ConditionLogicHelpText: string; 15 | export default ConditionLogicHelpText; 16 | } 17 | declare module "@salesforce/label/c.Delete" { 18 | var Delete: string; 19 | export default Delete; 20 | } 21 | declare module "@salesforce/label/c.FieldIsNotSupportedMessage" { 22 | var FieldIsNotSupportedMessage: string; 23 | export default FieldIsNotSupportedMessage; 24 | } 25 | declare module "@salesforce/label/c.For" { 26 | var For: string; 27 | export default For; 28 | } 29 | declare module "@salesforce/label/c.LackingPermissions" { 30 | var LackingPermissions: string; 31 | export default LackingPermissions; 32 | } 33 | declare module "@salesforce/label/c.ManagerEmptyMessage" { 34 | var ManagerEmptyMessage: string; 35 | export default ManagerEmptyMessage; 36 | } 37 | declare module "@salesforce/label/c.MultipleGroupsForRole" { 38 | var MultipleGroupsForRole: string; 39 | export default MultipleGroupsForRole; 40 | } 41 | declare module "@salesforce/label/c.Name" { 42 | var Name: string; 43 | export default Name; 44 | } 45 | declare module "@salesforce/label/c.NonePicklistValueLabel" { 46 | var NonePicklistValueLabel: string; 47 | export default NonePicklistValueLabel; 48 | } 49 | declare module "@salesforce/label/c.OWDReadWrite" { 50 | var OWDReadWrite: string; 51 | export default OWDReadWrite; 52 | } 53 | declare module "@salesforce/label/c.OwnerAdminModify" { 54 | var OwnerAdminModify: string; 55 | export default OwnerAdminModify; 56 | } 57 | declare module "@salesforce/label/c.PublicGroups" { 58 | var PublicGroups: string; 59 | export default PublicGroups; 60 | } 61 | declare module "@salesforce/label/c.Queues" { 62 | var Queues: string; 63 | export default Queues; 64 | } 65 | declare module "@salesforce/label/c.Read" { 66 | var Read: string; 67 | export default Read; 68 | } 69 | declare module "@salesforce/label/c.ReadWrite" { 70 | var ReadWrite: string; 71 | export default ReadWrite; 72 | } 73 | declare module "@salesforce/label/c.Reason" { 74 | var Reason: string; 75 | export default Reason; 76 | } 77 | declare module "@salesforce/label/c.RelatedUsers" { 78 | var RelatedUsers: string; 79 | export default RelatedUsers; 80 | } 81 | declare module "@salesforce/label/c.Roles" { 82 | var Roles: string; 83 | export default Roles; 84 | } 85 | declare module "@salesforce/label/c.Search" { 86 | var Search: string; 87 | export default Search; 88 | } 89 | declare module "@salesforce/label/c.SearchFor" { 90 | var SearchFor: string; 91 | export default SearchFor; 92 | } 93 | declare module "@salesforce/label/c.SetAccessLevel" { 94 | var SetAccessLevel: string; 95 | export default SetAccessLevel; 96 | } 97 | declare module "@salesforce/label/c.SupportedAddCapabilitiesEmptyMessage" { 98 | var SupportedAddCapabilitiesEmptyMessage: string; 99 | export default SupportedAddCapabilitiesEmptyMessage; 100 | } 101 | declare module "@salesforce/label/c.SupportedButtonsEmptyMessage" { 102 | var SupportedButtonsEmptyMessage: string; 103 | export default SupportedButtonsEmptyMessage; 104 | } 105 | declare module "@salesforce/label/c.SupportedEditCapabilitiesEmptyMessage" { 106 | var SupportedEditCapabilitiesEmptyMessage: string; 107 | export default SupportedEditCapabilitiesEmptyMessage; 108 | } 109 | declare module "@salesforce/label/c.TooManyResultsMessage" { 110 | var TooManyResultsMessage: string; 111 | export default TooManyResultsMessage; 112 | } 113 | declare module "@salesforce/label/c.Type" { 114 | var Type: string; 115 | export default Type; 116 | } 117 | declare module "@salesforce/label/c.UserOrGroup" { 118 | var UserOrGroup: string; 119 | export default UserOrGroup; 120 | } 121 | declare module "@salesforce/label/c.Users" { 122 | var Users: string; 123 | export default Users; 124 | } 125 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/schema.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/schema" { 2 | /** 3 | * Identifier for an object. 4 | */ 5 | export interface ObjectId { 6 | /** The object's API name. */ 7 | objectApiName: string; 8 | } 9 | 10 | /** 11 | * Identifier for an object's field. 12 | */ 13 | export interface FieldId { 14 | /** The field's API name. */ 15 | fieldApiName: string; 16 | /** The object's API name. */ 17 | objectApiName: string; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/staticresources.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@salesforce/resourceUrl/SiteSamples" { 2 | var SiteSamples: string; 3 | export default SiteSamples; 4 | } 5 | -------------------------------------------------------------------------------- /.sfdx/typings/lwc/user.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * User's Id. 3 | */ 4 | declare module "@salesforce/user/Id" { 5 | const id: string; 6 | export default id; 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.nodePath": "/Users/aedelstein/.vscode/extensions/salesforce.salesforcedx-vscode-lwc-47.17.1/node_modules" 3 | } -------------------------------------------------------------------------------- /Calculation__c.json: -------------------------------------------------------------------------------- 1 | { 2 | "records": [ 3 | { 4 | "attributes": { 5 | "type": "Calculation__c", 6 | "referenceId": "Calculation__cRef1" 7 | }, 8 | "Name": "Join Two Strings", 9 | "Formula__c": "'string1' + 'string2'" 10 | }, 11 | { 12 | "attributes": { 13 | "type": "Calculation__c", 14 | "referenceId": "Calculation__cRef2" 15 | }, 16 | "Name": "Extract Last Name", 17 | "Formula__c": "RIGHT($Record.Name, LEN($Record.Name) - FIND(\" \", $Record.Name))", 18 | "Context_Object_Type__c": "Contact", 19 | "Type__c": "ContactClean" 20 | }, 21 | { 22 | "attributes": { 23 | "type": "Calculation__c", 24 | "referenceId": "Calculation__cRef3" 25 | }, 26 | "Name": "Extract First Name", 27 | "Formula__c": "LEFT($Record.Name, FIND( \" \", $Record.Name ) -1 )", 28 | "Context_Object_Type__c": "Contact", 29 | "Type__c": "ContactClean" 30 | }, 31 | { 32 | "attributes": { 33 | "type": "Calculation__c", 34 | "referenceId": "Calculation__cRef4" 35 | }, 36 | "Name": "Simple Math", 37 | "Formula__c": "5+ 10" 38 | }, 39 | { 40 | "attributes": { 41 | "type": "Calculation__c", 42 | "referenceId": "Calculation__cRef5" 43 | }, 44 | "Name": "Is Today a Birthday" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Formula Builder 2 | 3 | It is an LWC that allows user to created formulas just like you do with standard formula builder when you create a formula field. User is able to insert field from context object or allowed context types like (User, Profile or Organization), Function (AND, OR, CONTAINS, etc...) and operator (+, -, ==, etc...) from predetermined picklist. 4 | 5 | ## Supported attributes 6 | 7 | ### formulaString 8 | Allows to specify formula value this component will be initialized with. 9 | ### contextObjectType 10 | Standard or custom object API name, determines set of fields which user will be able to choose from 'Insert Field' picklist 11 | ### supportedSystemTypes 12 | Comma separated list of Object API Names, which will be used as context variables and user will also be able to choose those objects fields. Usually it is User, Profile or Organization. 13 | 14 | ## Formula Evaluator 15 | 16 | Apex class that is responsible for actually evaluation a formula on run time based on passed in formula and context information. It supports context objects mentioned above and custom context variables, where developer is able to specify some custom constants. 17 | 18 | ### Supported Context Data 19 | 20 | $Record - put context record.Id here if your formula should take data from real record on run time 21 | anyCustomLiteral - attribute name and value, which can later be used while evaluating a formula. 22 | 23 | #### Example of context configuration: 24 | 25 | `Account acc = new Account(Name = 'Test acc', NumberOfEmployees = 11); 26 | insert acc; 27 | List context = new List(); 28 | context.add(new ContextWrapper('$Record', acc.Id)); 29 | context.add(new ContextWrapper('contextVariableOne', '30')); 30 | context.add(new ContextWrapper('contextVariableTwo', '45'));` 31 | 32 | #### Example of evaluating a formula 33 | `String stringContext = JSON.serialize(context); 34 | String result = FormulaEvaluator.parseFormula('$Record.NumberOfEmployees + contextVariableOne + contextVariableTwo', stringContext);` 35 | 36 | After evaluating above formula with its contexts it will return 11 + 30 + 45 = 86 as a result 37 | ### Supported Formula Functions 38 | 39 | Currently formula evaluator supports following functions, but this list can be extended by a developer: 40 | 41 | `'AND', 'OR', 'NOT', 'XOR', 'IF', 'CASE', 'LEN', 'SUBSTRING', 'LEFT', 'RIGHT', 42 | 'ISBLANK', 'ISPICKVAL', 'CONVERTID', 'ABS', 'ROUND', 'CEILING', 'FLOOR', 'SQRT', 'ACOS', 43 | 'ASIN', 'ATAN', 'COS', 'SIN', 'TAN', 'COSH', 'SINH', 'TANH', 'EXP', 'LOG', 'LOG10', 'RINT', 44 | 'SIGNUM', 'INTEGER', 'POW', 'MAX', 'MIN', 'MOD', 'TEXT', 'DATETIME', 'DECIMAL', 'BOOLEAN', 45 | 'DATE', 'DAY', 'MONTH', 'YEAR', 'HOURS', 'MINUTES', 'SECONDS', 'ADDDAYS', 'ADDMONTHS', 46 | 'ADDYEARS', 'ADDHOURS', 'ADDMINUTES', 'ADDSECONDS', 'CONTAINS', 'FIND', 'LOWER', 'UPPER' 47 | , 'MID', 'SUBSTITUTE', 'TRIM', 'VALUE', 'CONCATENATE'` 48 | 49 | ### Supported Formula Operators 50 | 51 | Currently formula evaluator supports following operators, but this list can be extended by a developer: 52 | 53 | `'+', '-', '/', '*', '==', '!=', '>', '<', '>=', '<=', '<>'` 54 | 55 | # Expression Builder 56 | 57 | It is an LWC component that allows user to create expressions by selecting fields, operators and fieldValues. It acts like standard expression builder. 58 | 59 | ## Component description 60 | 61 | At background it generates formula string that is later being processed by formula builder. 62 | 63 | ### Supported Conditions 64 | 65 | All Conditions Are Met - result of evaluating this expression will return 'true' only if *ALL* expressions return true 66 | Any Condition Is Met - result of evaluating this expression will return 'true' only if *ANY* expressions return true 67 | 68 | ### Supported Component Attributes 69 | 70 | #### formulaString 71 | Used to initialize a component with predetermined value or stores formula for current component state 72 | #### addButtonLabel 73 | Label for button to add new lines in expression builder 74 | #### contextObjectType 75 | API name of context object, it determines fields user will be able to choose in "Field" picklist 76 | #### supportedSystemTypes 77 | Comma separated list of Object API Names, which will be used as context variables and user will also be able to choose those objects fields. Usually it is User, Profile or Organization. 78 | -------------------------------------------------------------------------------- /config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "aedelstein Company", 3 | "edition": "Developer", 4 | "features": [], 5 | "settings": { 6 | "orgPreferenceSettings": { 7 | "s1DesktopEnabled": true 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /force-app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/.DS_Store -------------------------------------------------------------------------------- /force-app/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/.DS_Store -------------------------------------------------------------------------------- /force-app/main/default/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/.DS_Store -------------------------------------------------------------------------------- /force-app/main/default/classes/AssembleInputParams.cls: -------------------------------------------------------------------------------- 1 | 2 | public with sharing class AssembleInputParams { 3 | private static final Integer NUMBER_OF_PARAMS = 4; 4 | 5 | @InvocableMethod 6 | public static List execute(List requestList) { 7 | System.debug('entering AssembleInputParams'); 8 | Map requestListMap = (Map) JSON.deserialize(JSON.serialize(requestList[0]), Map.class); 9 | 10 | //Create a Results object to hold the return values 11 | Results response = new Results(); 12 | List params = new List(); 13 | for (Integer i = 1; i <= NUMBER_OF_PARAMS; i++) { 14 | String paramKey = (String) requestListMap.get('param' + i + 'Key'); 15 | String paramValue = (String) requestListMap.get('param' + i + 'Value'); 16 | if ((paramKey != null && paramValue == null) || (paramKey == null && paramValue != null)) 17 | throw new InvocableActionException('You need to provide both a param1Key and a param1Value. Currently you are only providing 1 of those'); 18 | if (paramKey != null && paramValue != null) { 19 | params.add(new ContextWrapper(paramKey, paramValue)); 20 | } 21 | } 22 | 23 | response.jsonParams = JSON.serialize(params); 24 | 25 | //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) 26 | List responseWrapper = new List(); 27 | responseWrapper.add(response); 28 | System.debug('response is: ' + response); 29 | System.debug('responseWrapper is: ' + responseWrapper); 30 | 31 | return responseWrapper; 32 | 33 | } 34 | 35 | public class InvocableActionException extends Exception { 36 | } 37 | 38 | 39 | public class Requests { 40 | 41 | @InvocableVariable 42 | public String param1Key; 43 | 44 | @InvocableVariable 45 | public String param1Value; 46 | 47 | @InvocableVariable 48 | public String param2Key; 49 | 50 | @InvocableVariable 51 | public String param2Value; 52 | 53 | @InvocableVariable 54 | public String param3Key; 55 | 56 | @InvocableVariable 57 | public String param3Value; 58 | 59 | @InvocableVariable 60 | public String param4Key; 61 | 62 | @InvocableVariable 63 | public String param4Value; 64 | 65 | } 66 | 67 | public class Results { 68 | 69 | @InvocableVariable 70 | public String jsonParams; 71 | 72 | } 73 | 74 | public class ContextWrapper { 75 | public String name; 76 | public String value; 77 | 78 | public ContextWrapper(String name, String value) { 79 | this.name = name; 80 | this.value = value; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /force-app/main/default/classes/AssembleInputParams.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/EvaluateFormula.cls: -------------------------------------------------------------------------------- 1 | public with sharing class EvaluateFormula { 2 | 3 | @InvocableMethod 4 | public static List evaluate(List requestList) { 5 | System.debug('entering Evaluate Formula'); 6 | 7 | String formulaString = requestList[0].formulaString; 8 | String recordId = requestList[0].recordId; 9 | String contextDataString = JSON.serialize(generateContextByRecordId(recordId)); 10 | 11 | String result = FormulaEvaluator.parseFormula(formulaString, contextDataString); 12 | //TO DO: need to fix this to return non-integer values effectively. we may need to pass in an indicator as to what 13 | //what the input type is 14 | 15 | //Create a Results object to hold the return values 16 | Results response = new Results(); 17 | response.stringResult = result; 18 | 19 | //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) 20 | List responseWrapper = new List(); 21 | responseWrapper.add(response); 22 | System.debug('response is: ' + response); 23 | System.debug('responseWrapper is: ' + responseWrapper); 24 | 25 | return responseWrapper; 26 | 27 | } 28 | 29 | private static List generateContextByRecordId(String recordId) { 30 | List context = new List(); 31 | if(!String.isBlank(recordId)){ 32 | context.add(new FormulaEvaluator.ContextWrapper('$Record', recordId)); 33 | } 34 | return context; 35 | } 36 | 37 | public class Requests { 38 | 39 | @InvocableVariable 40 | public String formulaString; 41 | 42 | @InvocableVariable 43 | public String recordId; 44 | 45 | } 46 | 47 | public class Results { 48 | 49 | 50 | @InvocableVariable 51 | public String stringResult; 52 | 53 | @InvocableVariable 54 | public Decimal numberResult; 55 | 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/EvaluateFormula.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExpressionBuilder.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExpressionBuilderTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class ExpressionBuilderTest { 3 | @IsTest 4 | static void assembleAndDisassemblyFormulaStringTest() { 5 | String expression = '1 AND (2 OR 3) AND 4 AND (5 OR 6 OR (7 AND 8)) AND (1 OR 2)'; 6 | String testData = '[{"objectType":"Account","fieldName":"$Account.AccountNumber","dataType":"String","operator":"starts_with","parameter":"1"}, ' + 7 | '{"objectType":"Account","fieldName":"$Account.AccountSource","dataType":"String","operator":"not_equal_to","parameter":"A"}, ' + 8 | '{"objectType":"Account","fieldName":"$Account.AnnualRevenue","dataType":"Currency","operator":"less_then","parameter":"2"}, ' + 9 | '{"objectType":"Account","fieldName":"$Account.Id","dataType":"String","operator":"equals","parameter":"3"}, ' + 10 | '{"objectType":"User","fieldName":"$User.AccountNumber","dataType":"String","operator":"contains","parameter":"1"},' + 11 | '{"objectType":"Organization","fieldName":"$Organization.AccountSource","dataType":"String","operator":"does_not_contain","parameter":"A"}, ' + 12 | '{"objectType":"Account","fieldName":"$Account.AnnualRevenue","dataType":"Currency","operator":"includes","parameter":"2"},' + 13 | '{"objectType":"Account","fieldName":"$Account.Id","dataType":"String","operator":"excludes","parameter":"3"}]'; 14 | 15 | String assembleResult = ExpressionBuilder.assembleFormulaString(expression, 'CUSTOM', testData); 16 | String expectResult = 'AND(LEFT($Account.AccountNumber, 1) == TEXT("1"), OR($Account.AccountSource != TEXT("A"), ' + 17 | '$Account.AnnualRevenue < DECIMAL("2")), $Account.Id == TEXT("3"), OR(CONTAINS($User.AccountNumber, TEXT("1")), ' + 18 | '!CONTAINS($Organization.AccountSource, TEXT("A")), AND(INCLUDES($Account.AnnualRevenue, DECIMAL("2")), ' + 19 | '!INCLUDES($Account.Id, TEXT("3")))), OR(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A")))'; 20 | 21 | System.assertEquals(expectResult, assembleResult); 22 | 23 | Map disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 24 | List data = (List)disassemblyResult.get('expressionLines'); 25 | 26 | System.assertEquals('CUSTOM', String.valueOf(disassemblyResult.get('logicType'))); 27 | //Switching off custom logic asserts due to not stable work 28 | //System.assertEquals(expression, String.valueOf(disassemblyResult.get('customLogic'))); 29 | System.assertEquals(8, data.size()); 30 | 31 | 32 | assembleResult = ExpressionBuilder.assembleFormulaString('', 'AND', testData); 33 | expectResult = 'AND(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A"), ' + 34 | '$Account.AnnualRevenue < DECIMAL("2"), $Account.Id == TEXT("3"), CONTAINS($User.AccountNumber, TEXT("1")), ' + 35 | '!CONTAINS($Organization.AccountSource, TEXT("A")), INCLUDES($Account.AnnualRevenue, DECIMAL("2")), !INCLUDES($Account.Id, TEXT("3")))'; 36 | 37 | System.assertEquals(expectResult, assembleResult); 38 | 39 | disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 40 | data = (List)disassemblyResult.get('expressionLines'); 41 | 42 | System.assertEquals('AND', String.valueOf(disassemblyResult.get('logicType'))); 43 | System.assertEquals('', String.valueOf(disassemblyResult.get('customLogic'))); 44 | System.assertEquals(8, data.size()); 45 | 46 | assembleResult = ExpressionBuilder.assembleFormulaString('', 'OR', testData); 47 | expectResult = 'OR(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A"), ' + 48 | '$Account.AnnualRevenue < DECIMAL("2"), $Account.Id == TEXT("3"), CONTAINS($User.AccountNumber, TEXT("1")), ' + 49 | '!CONTAINS($Organization.AccountSource, TEXT("A")), INCLUDES($Account.AnnualRevenue, DECIMAL("2")), !INCLUDES($Account.Id, TEXT("3")))'; 50 | 51 | System.assertEquals(expectResult, assembleResult); 52 | 53 | disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 54 | data = (List)disassemblyResult.get('expressionLines'); 55 | 56 | System.assertEquals('OR', String.valueOf(disassemblyResult.get('logicType'))); 57 | System.assertEquals('', String.valueOf(disassemblyResult.get('customLogic'))); 58 | System.assertEquals(8, data.size()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /force-app/main/default/classes/ExpressionBuilderTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FieldPickerController.cls: -------------------------------------------------------------------------------- 1 | global with sharing class FieldPickerController { 2 | 3 | @AuraEnabled(cacheable=true) 4 | public static List getObjects(List availableObjectTypes) { 5 | 6 | List result = new List(); 7 | Map allTypes = new Map(); 8 | 9 | if (availableObjectTypes == null || availableObjectTypes.size() == 0) { 10 | allTypes = Schema.getGlobalDescribe(); 11 | availableObjectTypes = new List(allTypes.keySet()); 12 | } else if (availableObjectTypes.size() > 0) { 13 | for (String curType : availableObjectTypes) { 14 | allTypes.put(curType, ((SObject) (Type.forName('Schema.' + curType).newInstance())).getSObjectType()); 15 | } 16 | } 17 | 18 | for (String objType : availableObjectTypes) { 19 | 20 | Schema.DescribeSObjectResult describeObject = allTypes.get(objType).getDescribe(); 21 | String objectType = describeObject.getName(); 22 | String objectLabel = describeObject.getLabel(); 23 | Boolean isCustom = describeObject.isCustom(); 24 | 25 | if (isCustom || allowedStandardObjects.containsKey(objectType)) { 26 | result.add(new Member(objectType, objectLabel)); 27 | } 28 | 29 | result.sort(); 30 | 31 | } 32 | 33 | return result; 34 | 35 | } 36 | @AuraEnabled(cacheable=true) 37 | public static List getPicklistValues(String objectApiName, String fieldName) { 38 | 39 | List options = new List(); 40 | 41 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(objectApiName).getDescribe(); 42 | Map fieldMap = describeObject.fields.getMap(); 43 | List values = fieldMap.get(fieldName).getDescribe().getPickListValues(); 44 | 45 | for (Schema.PicklistEntry a : values) { 46 | options.add(new Member(a.getValue(), a.getLabel())); 47 | } 48 | 49 | options.sort(); 50 | 51 | return options; 52 | } 53 | 54 | 55 | public class Member implements Comparable { 56 | 57 | @AuraEnabled 58 | public String label; 59 | @AuraEnabled 60 | public String value; 61 | 62 | public Member(String value, String label) { 63 | this.label = label; 64 | this.value = value; 65 | } 66 | 67 | public Integer compareTo(Object compareTo) { 68 | 69 | Member curMember = (Member) compareTo; 70 | 71 | if (label == curMember.label) { 72 | return 0; 73 | } 74 | if (label > curMember.label) { 75 | return 1; 76 | } 77 | return -1; 78 | 79 | } 80 | 81 | } 82 | @TestVisible 83 | private static Map allowedStandardObjects = new Map{ 84 | 'Account' => 'Account', 85 | 'AccountPartner' => 'Account Partner', 86 | 'Asset' => 'Asset', 87 | 'AssetRelationship' => 'Asset Relationship', 88 | 'AssignedResource' => 'Assigned Resource', 89 | 'Campaign' => 'Campaign', 90 | 'CampaignMember' => 'Campaign Member', 91 | 'Case' => 'Case', 92 | 'Contact' => 'Contact', 93 | 'ContactRequest' => 'Contact Request', 94 | 'ContentDocument' => 'File', 95 | 'ContentVersion' => 'File', 96 | 'ContentWorkspace' => 'Library', 97 | 'Contract' => 'Contract', 98 | 'ContractContactRole' => 'Contract Contact Role', 99 | 'Image' => 'Image', 100 | 'Individual' => 'Individual', 101 | 'Lead' => 'Lead', 102 | 'MaintenanceAsset' => 'Maintenance Asset', 103 | 'MaintenancePlan' => 'Maintenance Plan', 104 | 'Note' => 'Note', 105 | 'OperatingHours' => 'Operating Hours', 106 | 'Opportunity' => 'Opportunity', 107 | 'OpportunityLineItem' => 'Opportunity Product', 108 | 'OpportunityPartner' => 'Opportunity Partner', 109 | 'Order' => 'Order', 110 | 'OrderItem' => 'Order Product', 111 | 'Partner' => 'Partner', 112 | 'Pricebook2' => 'Price Book', 113 | 'PricebookEntry' => 'Price Book Entry', 114 | 'Product2' => 'Product', 115 | 'RecordType' => 'Record Type', 116 | 'ResourceAbsence' => 'Resource Absence', 117 | 'ResourcePreference' => 'Resource Preference', 118 | 'ReturnOrder' => 'Return Order', 119 | 'ReturnOrderLineItem' => 'Return Order Line Item', 120 | 'ServiceAppointment' => 'Service Appointment', 121 | 'ServiceCrew' => 'Service Crew', 122 | 'ServiceCrewMember' => 'Service Crew Member', 123 | 'ServiceResource' => 'Service Resource', 124 | 'ServiceResourceCapacity' => 'Resource Capacity', 125 | 'ServiceResourceSkill' => 'Service Resource Skill', 126 | 'ServiceTerritory' => 'Service Territory', 127 | 'ServiceTerritoryLocation' => 'Service Territory Location', 128 | 'ServiceTerritoryMember' => 'Service Territory Member', 129 | 'Shift' => 'Shift', 130 | 'Shipment' => 'Shipment', 131 | 'SkillRequirement' => 'Skill Requirement', 132 | 'TimeSheet' => 'Time Sheet', 133 | 'TimeSheetEntry' => 'Time Sheet Entry', 134 | 'TimeSlot' => 'Time Slot', 135 | 'User' => 'User', 136 | 'WorkOrder' => 'Work Order', 137 | 'WorkOrderLineItem' => 'Work Order Line Item', 138 | 'WorkType' => 'Work Type' 139 | }; 140 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/FieldPickerController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FieldPickerControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class FieldPickerControllerTest { 3 | private static final String OBJECT_NAME = 'Account'; 4 | private static final String PICKLIST_FIELD_NAME = 'AccountSource'; 5 | 6 | @IsTest 7 | static void testGetObjects() { 8 | //Get two supported and one not supported object 9 | List members = FieldPickerController.getObjects(new List{ 10 | 'Account', 'User' 11 | }); 12 | System.assertEquals(2, members.size()); 13 | System.assertEquals('Account', members[0].label); 14 | System.assertEquals('User', members[1].label); 15 | //Get all objects 16 | members = FieldPickerController.getObjects(NULL); 17 | for (FieldPickerController.Member m : members) { 18 | System.debug(m.label); 19 | } 20 | //All supported objects plus all custom are different on different types of orgs, so commenting this line out. 21 | //System.assertEquals(true, members.size() >= FieldPickerController.allowedStandardObjects.size()); 22 | } 23 | 24 | @IsTest 25 | static void testGetPicklistValues() { 26 | List members = FieldPickerController.getPicklistValues(OBJECT_NAME, PICKLIST_FIELD_NAME); 27 | 28 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(OBJECT_NAME).getDescribe(); 29 | Map fieldMap = describeObject.fields.getMap(); 30 | List values = fieldMap.get(PICKLIST_FIELD_NAME).getDescribe().getPickListValues(); 31 | 32 | System.assertEquals(members.size(), values.size()); 33 | } 34 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/FieldPickerControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaBuilderController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class FormulaBuilderController { 2 | 3 | @AuraEnabled(cacheable=true) 4 | public static List getFieldList(String objectName) { 5 | List result = new List(); 6 | 7 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(objectName).getDescribe(); 8 | Map fields = describeObject.fields.getMap(); 9 | 10 | for(Schema.sObjectField field : fields.values()) { 11 | result.add(field.getDescribe().getName()); 12 | } 13 | 14 | return result; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaBuilderController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaBuilderControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class FormulaBuilderControllerTest { 3 | @IsTest 4 | static void getFieldListTest() { 5 | List result = FormulaBuilderController.getFieldList('Opportunity'); 6 | System.assertNotEquals(NULL, result, 'Result not NULL'); 7 | System.assertNotEquals(0, result.size(), 'Result size > 0'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaBuilderControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaEvaluator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/FormulaEvaluatorTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/RegExps.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchUtils.cls: -------------------------------------------------------------------------------- 1 | global with sharing class SearchUtils { 2 | 3 | static final Map TYPE_TO_SOBJECT = new Map{ 4 | 'User' => 'User', 5 | 'Role_subordinates' => 'UserRole', 6 | 'Role' => 'UserRole', 7 | 'Group' => 'Group', 8 | 'Queue' => 'Group' 9 | }; 10 | 11 | static final Map TYPE_TO_QUERY_CRITERIA = new Map{ 12 | 'User' => 'WHERE IsActive = true AND Name LIKE (searchString)', 13 | 'Role_subordinates' => 'WHERE Name LIKE (searchString)', 14 | 'Role' => 'WHERE Name LIKE (searchString)', 15 | 'Group' => 'WHERE (Type = \'Public\' OR Type = \'Regular\') AND Name LIKE (searchString)', 16 | 'Queue' => 'WHERE Type = \'Queue\' AND Name LIKE (searchString)' 17 | }; 18 | 19 | static final List OTHER_TYPES = new List{ 20 | 'RelatedUsers', 'Owner', 'Creator' 21 | }; 22 | 23 | public static final Map TYPE_TO_ID_FIELD = new Map{ 24 | 'User' => 'UserName', 25 | 'Group' => 'DeveloperName', 26 | 'Queue' => 'DeveloperName', 27 | 'Role' => 'DeveloperName', 28 | 'Role_subordinates' => 'DeveloperName' 29 | }; 30 | 31 | 32 | @AuraEnabled(cacheable=true) 33 | public static Map> searchMemberByType(List memberTypes, String searchString) { 34 | Map> resultMap = new Map>(); 35 | 36 | for (String curType : TYPE_TO_SOBJECT.keySet()) { 37 | 38 | if (!memberTypes.contains(curType)) { 39 | continue; 40 | } 41 | 42 | String queryString = 'SELECT ' + getQueriedFields(curType) + ' FROM ' + TYPE_TO_SOBJECT.get(curType) + ' ' + TYPE_TO_QUERY_CRITERIA.get(curType).replace('(searchString)', '\'%' + String.escapeSingleQuotes(searchString) + '%\''); 43 | 44 | List types = Database.query(queryString); 45 | List members = new List(); 46 | for (SObject t : types) { 47 | members.add(new Member((String) t.get('Name'), (String) t.get(getIdField(curType)))); 48 | } 49 | resultMap.put(curType, members); 50 | } 51 | 52 | return resultMap; 53 | } 54 | @AuraEnabled(cacheable=true) 55 | public static Map getSingleMembersByTypeAndId(String type, String id) { 56 | return getMembersByTypeAndId(new Map>{ 57 | type => new Set{ 58 | id 59 | } 60 | }); 61 | } 62 | public static Map getMembersByTypeAndId(Map> typeToIds) { 63 | 64 | Map results = new Map(); 65 | for (String sObjectTypeName : typeToIds.keySet()) { 66 | if (OTHER_TYPES.contains(sObjectTypeName)) { 67 | for (String curMember : typeToIds.get(sObjectTypeName)) { 68 | results.put(curMember, new Account(Name = curMember)); 69 | } 70 | } else { 71 | Set objectIds = typeToIds.get(sObjectTypeName); 72 | 73 | String idField = getIdField(sObjectTypeName); 74 | String queryString = 'SELECT ' + getQueriedFields(sObjectTypeName) + ' FROM ' + TYPE_TO_SOBJECT.get(sObjectTypeName) + ' WHERE ' + idField + ' IN: objectIds'; 75 | List members = Database.query(queryString); 76 | for (SObject curMember : members) { 77 | results.put((String) curMember.get(idField), curMember); 78 | } 79 | } 80 | } 81 | return results; 82 | } 83 | 84 | private static String getIdField(String objectType) { 85 | return TYPE_TO_ID_FIELD.containsKey(objectType) ? TYPE_TO_ID_FIELD.get(objectType) : 'Id'; 86 | } 87 | 88 | private static String getQueriedFields(String objectType) { 89 | String requiredFields = 'Id, Name'; 90 | if (TYPE_TO_ID_FIELD.containsKey(objectType)) { 91 | requiredFields += ', ' + TYPE_TO_ID_FIELD.get(objectType); 92 | } 93 | return requiredFields; 94 | } 95 | 96 | @AuraEnabled(cacheable=true) 97 | public static Map> describeSObjects(List types) { 98 | 99 | Map> objectToFieldDescribe = new Map>(); 100 | if (types == null || types.isEmpty()) { 101 | return objectToFieldDescribe; 102 | } 103 | 104 | Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types); 105 | 106 | for (Schema.DescribeSobjectResult res : results) { 107 | String objName = res.getName(); 108 | objectToFieldDescribe.put(objName, new List()); 109 | Map fieldMap = res.fields.getMap(); 110 | for (String fieldApiName : fieldMap.keySet()) { 111 | Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldApiName).getDescribe(); 112 | objectToFieldDescribe.get(res.getName()).add(new Member(fieldDescribe.getLabel(), fieldDescribe.getName(), objName, fieldDescribe.getType().name())); 113 | } 114 | } 115 | 116 | return objectToFieldDescribe; 117 | } 118 | 119 | global class Member { 120 | @AuraEnabled global String label; 121 | @AuraEnabled global String value; 122 | @AuraEnabled global String dataType; 123 | @AuraEnabled global String type; 124 | public Member(String label, String value) { 125 | this.label = label; 126 | this.value = value; 127 | } 128 | public Member(String label, String value, String type, String dataType) { 129 | this.label = label; 130 | this.value = value; 131 | this.dataType = dataType; 132 | this.type = type; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchUtils.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchUtilsTest.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class SearchUtilsTest { 3 | 4 | static final List TYPES_TO_SEARCH = new List{ 5 | 'User', 'Queue' 6 | }; 7 | static final String TEST_RECORD_NAME = 'testrecordname@test.te'; 8 | 9 | @testSetup 10 | static void setup() { 11 | List adminProfile = [SELECT Id FROM Profile WHERE Name = 'System Administrator']; 12 | User testUser = new User(Alias = 'test1', Email = 'testuser1@testorg.com', EmailEncodingKey = 'UTF-8', LastName = TEST_RECORD_NAME, LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US', ProfileId = adminProfile[0].Id, TimeZoneSidKey = 'America/Los_Angeles', UserName = TEST_RECORD_NAME); 13 | insert testUser; 14 | Group userGroup = new Group(Name = TEST_RECORD_NAME, type = 'Queue'); 15 | insert userGroup; 16 | QueuesObject queue = new QueueSObject(QueueID = userGroup.id, SobjectType = 'lead'); 17 | insert queue; 18 | } 19 | 20 | @IsTest 21 | static void testSearchMemberByType() { 22 | User testUser = [SELECT Id, Name, UserName FROM User WHERE Username = :TEST_RECORD_NAME]; 23 | Group testQueue = [SELECT Id, Name,DeveloperName FROM Group WHERE Type = 'Queue' AND Name = :TEST_RECORD_NAME]; 24 | 25 | Map typeToObject = new Map{ 26 | 'User' => testUser, 'Queue' => testQueue 27 | }; 28 | Map> searchResult = SearchUtils.searchMemberByType(TYPES_TO_SEARCH, TEST_RECORD_NAME); 29 | 30 | //We have added one record to each type, so expecting as many records as types 31 | System.assertEquals(TYPES_TO_SEARCH.size(), searchResult.size()); 32 | 33 | //For each type checking if search worked fine 34 | for (String typeName : TYPES_TO_SEARCH) { 35 | System.assertEquals(typeToObject.get(typeName).get(SearchUtils.TYPE_TO_ID_FIELD.get(typeName)), searchResult.get(typeName)[0].value); 36 | System.assertEquals(typeToObject.get(typeName).get('Name'), searchResult.get(typeName)[0].label); 37 | } 38 | 39 | //Checking that get name by id works fine 40 | Map> typeToIds = new Map>{ 41 | 'User' => (new Set{ 42 | testUser.UserName 43 | }), 'Queue' => (new Set{ 44 | testQueue.DeveloperName 45 | }) 46 | }; 47 | Map members = SearchUtils.getMembersByTypeAndId(typeToIds); 48 | 49 | System.assertEquals(TEST_RECORD_NAME, members.get(testUser.UserName).get('Name')); 50 | System.assertEquals(TEST_RECORD_NAME, members.get(testQueue.DeveloperName).get('Name')); 51 | } 52 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/SearchUtilsTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/UpdateField.cls: -------------------------------------------------------------------------------- 1 | public with sharing class UpdateField { 2 | 3 | @InvocableMethod(label='Update Field' configurationEditor='c:updateFieldCPE') 4 | public static List execute (List requests) { 5 | String objectId = requests[0].objectId; 6 | String fieldName = requests[0].fieldName; 7 | String formula = requests[0].formula; 8 | 9 | System.debug('requests is' + requests); 10 | List contextWrappers = new List(); 11 | contextWrappers.add(new ContextWrapper('$Record', objectId)); 12 | 13 | String fieldValue = FormulaEvaluator.parseFormula( formula, JSON.serialize(contextWrappers)); 14 | System.debug(fieldValue); 15 | String objectAPIName; 16 | String keyPrefix = objectId.substring(0,3); 17 | 18 | for (Schema.SObjectType obj : Schema.getGlobalDescribe().Values()){ 19 | String prefix = obj.getDescribe().getKeyPrefix(); 20 | if (prefix == keyPrefix && prefix.indexOf(keyPrefix) != -1){ 21 | objectAPIName = obj.getDescribe().getName(); 22 | break; 23 | } 24 | } 25 | 26 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(objectAPIName).getDescribe(); 27 | Map fields = describeObject.fields.getMap(); 28 | String fieldType = String.valueOf(fields.get( fieldName).getDescribe().type); 29 | 30 | sObject currentObject = Schema.getGlobalDescribe().get(objectAPIName).newSObject(); 31 | currentObject.put('Id', objectId); 32 | 33 | if (fieldType == 'BOOLEAN') { 34 | currentObject.put( fieldName, Boolean.valueOf(fieldValue)); 35 | } else if (fieldType == 'CURRENCY' || fieldType == 'PERCENT' || fieldType == 'DOUBLE') { 36 | currentObject.put( fieldName, Decimal.valueOf(fieldValue)); 37 | } else if (fieldType == 'DATE') { 38 | currentObject.put( fieldName, Date.valueOf(fieldValue)); 39 | } else if (fieldType == 'DATETIME') { 40 | currentObject.put( fieldName, Datetime.valueOf(fieldValue)); 41 | } else if (fieldType == 'INTEGER') { 42 | currentObject.put( fieldName, Integer.valueOf(fieldValue)); 43 | } else { 44 | currentObject.put( fieldName, fieldValue); 45 | } 46 | 47 | Database.SaveResult updateResult = Database.update(currentObject, true); 48 | 49 | /* if (!result.isSuccess()) { 50 | throw new UpdateFieldActionException(result.getErrors()[0].getMessage()); 51 | } */ 52 | 53 | //Create a Results object to hold the return values 54 | Response response = new Response(); 55 | 56 | //add the return values to the Results object 57 | response.isSuccess = updateResult.isSuccess(); 58 | response.errors = packErrorString(updateResult); 59 | 60 | //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) 61 | List responseWrapper= new List(); 62 | responseWrapper.add(response); 63 | return responseWrapper; 64 | 65 | 66 | 67 | 68 | } 69 | 70 | public static String packErrorString(Database.SaveResult saveResult) { 71 | String errorString = ''; 72 | for(Database.Error err : saveResult.getErrors()) { 73 | errorString = errorString + (' The following error has occurred.'); 74 | errorString = errorString + (' ' + err.getStatusCode() + ': ' + err.getMessage()); 75 | } 76 | return errorString; 77 | 78 | } 79 | 80 | public class Request { 81 | @InvocableVariable(required=true) 82 | public String objectId; 83 | 84 | @InvocableVariable(required=true) 85 | public String fieldName; 86 | 87 | @InvocableVariable(required=true) 88 | public String formula; 89 | } 90 | 91 | public class Response { 92 | @InvocableVariable 93 | public Boolean isSuccess; 94 | 95 | @InvocableVariable 96 | public String errors; 97 | 98 | 99 | } 100 | 101 | public class ContextWrapper { 102 | public String name; 103 | public String value; 104 | 105 | public ContextWrapper(String name, String value) { 106 | this.name = name; 107 | this.value = value; 108 | } 109 | } 110 | 111 | class UpdateFieldActionException extends Exception {} 112 | } 113 | -------------------------------------------------------------------------------- /force-app/main/default/classes/UpdateField.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/UpdateFieldTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class UpdateFieldTest { 3 | @isTest 4 | static void UpdateFieldActionRequestTest() { 5 | Opportunity opp = new Opportunity(); 6 | opp.Name = 'Test Opportunity'; 7 | opp.Amount = 15000.05; 8 | opp.CloseDate = Date.today().addDays(-30); 9 | opp.StageName = 'Prospecting'; 10 | opp.IsPrivate = false; 11 | 12 | insert opp; 13 | 14 | UpdateFieldAction.Request request = new UpdateFieldAction.Request(); 15 | request.objectId = opp.Id; 16 | request.fieldName = 'Amount'; 17 | request.formula = '$Record.Amount + 200'; 18 | UpdateFieldAction.updateField(new List{request}); 19 | 20 | System.assertEquals(15200.05, [SELECT Amount FROM Opportunity WHERE Id =: opp.Id LIMIT 1].Amount, 'Update decimal field'); 21 | 22 | request.fieldName = 'IsPrivate'; 23 | request.formula = '15 > 10'; 24 | UpdateFieldAction.updateField(new List{request}); 25 | 26 | System.assertEquals(true, [SELECT IsPrivate FROM Opportunity WHERE Id =: opp.Id LIMIT 1].IsPrivate, 'Update boolean field'); 27 | 28 | request.fieldName = 'CloseDate'; 29 | request.formula = '$TODAY'; 30 | UpdateFieldAction.updateField(new List{request}); 31 | 32 | System.assertEquals(System.today(), [SELECT CloseDate FROM Opportunity WHERE Id =: opp.Id LIMIT 1].CloseDate, 'Update date field'); 33 | 34 | request.fieldName = 'Name'; 35 | request.formula = '$Record.Name + "1"'; 36 | UpdateFieldAction.updateField(new List{request}); 37 | 38 | System.assertEquals('Test Opportunity1', [SELECT Name FROM Opportunity WHERE Id =: opp.Id LIMIT 1].Name, 'Update string field'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /force-app/main/default/classes/UpdateFieldTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/flows/TestFlow_EvaluateFormula.flow-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Evaluate_Formula 5 | 6 | 476 7 | 194 8 | EvaluateFormula 9 | apex 10 | 11 | screenOutput 12 | 13 | 14 | formulaString 15 | 16 | Enter_Formula 17 | 18 | 19 | 20 | recordId 21 | 22 | record_id 23 | 24 | 25 | 26 | formulaResult 27 | stringResult 28 | 29 | 30 | 31 | Evaluate_Formula2 32 | 33 | 477 34 | 354 35 | EvaluateFormula 36 | apex 37 | 38 | screenOutput 39 | 40 | 41 | formulaString 42 | 43 | formulaString 44 | 45 | 46 | 47 | recordId 48 | 49 | record_id 50 | 51 | 52 | 53 | formulaResult 54 | stringResult 55 | 56 | 57 | TestFlow_EvaluateFormula {!$Flow.CurrentDateTime} 58 | 59 | 60 | BuilderType 61 | 62 | LightningFlowBuilder 63 | 64 | 65 | 66 | OriginBuilderType 67 | 68 | LightningFlowBuilder 69 | 70 | 71 | Flow 72 | 73 | screenBuildFormula 74 | 75 | 162 76 | 342 77 | true 78 | true 79 | true 80 | 81 | Evaluate_Formula2 82 | 83 | 84 | true 85 | true 86 | 87 | 88 | screenInput 89 | 90 | 164 91 | 51 92 | true 93 | true 94 | true 95 | 96 | Evaluate_Formula 97 | 98 | 99 | Enter_Formula 100 | String 101 | Enter Formula 102 | InputField 103 | false 104 | 105 | true 106 | true 107 | 108 | 109 | screenOutput 110 | 111 | 775 112 | 208 113 | true 114 | true 115 | true 116 | 117 | dispResult 118 | <p>The result is: {!formulaResult}</p> 119 | DisplayText 120 | 121 | true 122 | true 123 | 124 | screenBuildFormula 125 | Draft 126 | 127 | formulaResult 128 | String 129 | false 130 | false 131 | false 132 | 133 | 134 | formulaString 135 | String 136 | false 137 | false 138 | false 139 | 140 | 141 | record_id 142 | String 143 | false 144 | true 145 | true 146 | 147 | 148 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/buttonUtils/buttonUtils.js: -------------------------------------------------------------------------------- 1 | export { 2 | generateCapabilityColumns, 3 | splitValues 4 | }; 5 | 6 | const generateCapabilityColumns = (labels) => { 7 | let labelsArray = labels.replace(/ /g, '').split(','); 8 | return labelsArray.map(curLabel => { 9 | return getColumnDescriptor(curLabel); 10 | }); 11 | }; 12 | 13 | const getColumnDescriptor = (curButtonLabel) => { 14 | return { 15 | type: 'button', 16 | label: curButtonLabel, 17 | typeAttributes: { 18 | label: curButtonLabel, 19 | name: curButtonLabel, //this is used to determine an apex method to call 20 | variant: 'neutral', 21 | disabled: {fieldName: curButtonLabel.replace(/ /g, '') + 'buttonDisabled'} 22 | }, 23 | initialWidth: 120 //TODO: Calculate based on content 24 | } 25 | }; 26 | 27 | const splitValues = (originalString) => { 28 | if (originalString) { 29 | return originalString.replace(/ /g, '').split(','); 30 | } else { 31 | return []; 32 | } 33 | }; 34 | 35 | 36 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/buttonUtils/buttonUtils.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/expressionBuilder/expressionBuilder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/expressionBuilder/expressionBuilder.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | expressionBuilder 5 | true 6 | expressionBuilder 7 | 8 | lightning__RecordPage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/expressionLine/expressionLine.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/expressionLine/expressionLine.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track} from 'lwc'; 2 | 3 | export default class expressionLine extends LightningElement { 4 | 5 | @api objectType; 6 | @api operator; 7 | @api value; 8 | @api expressionId; 9 | @api expressionIndex; 10 | @api availableMergeFields = []; 11 | @track _fields = []; 12 | 13 | @track currentField; 14 | @track _fieldName; 15 | @track allOperators = [ 16 | {value: 'equals',label: 'Equals',types: 'ID,BOOLEAN,REFERENCE,STRING,EMAIL,PICKLIST,TEXTAREA,DATETIME,PHONE,DOUBLE,ADDRESS,INTEGER,URL'}, 17 | {value: 'not_equal_to',label: 'Not Equal To', types: 'ID,BOOLEAN,REFERENCE,STRING,EMAIL,PICKLIST,TEXTAREA,DATETIME,PHONE,DOUBLE,ADDRESS,INTEGER,URL'}, 18 | {value: 'greater_then', label: 'Greater than', types: 'DOUBLE,INTEGER,DATETIME'}, 19 | {value: 'greater_or_equal', label: 'Greater Or Equal', types: 'DOUBLE,INTEGER,DATETIME'}, 20 | {value: 'less_then', label: 'Less Than', types: 'DOUBLE,INTEGER,DATETIME'}, 21 | {value: 'less_or_equal', label: 'Less Or Equal', types: 'DOUBLE,INTEGER,DATETIME'}, 22 | {value: 'contains', label: 'Contains', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'}, 23 | {value: 'starts_with', label: 'Starts with', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'}, 24 | {value: 'end_with', label: 'End with', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'} 25 | ]; 26 | 27 | @track filterValue = ''; 28 | 29 | initialized = false; 30 | 31 | renderedCallback() { 32 | if (this.initialized) { 33 | return; 34 | } 35 | this.initialized = true; 36 | let listId = this.template.querySelector('datalist').id; 37 | this.template.querySelector("input").setAttribute("list", listId); 38 | } 39 | 40 | @api 41 | get fields() { 42 | return this._fields; 43 | } 44 | 45 | set fields(value) { 46 | this._fields = value; 47 | this.setCurrentField(); 48 | } 49 | 50 | 51 | @api 52 | get fieldName() { 53 | return this._fieldName; 54 | 55 | } 56 | 57 | set fieldName(value) { 58 | this._fieldName = value; 59 | this.setCurrentField(); 60 | } 61 | 62 | get availableOperators() { 63 | if (this.currentField) { 64 | return this.allOperators.filter(curOperator => curOperator.types.includes(this.currentField.dataType.toUpperCase())); 65 | } else { 66 | return []; 67 | } 68 | } 69 | 70 | selectField(event) { 71 | let eventValue = event.detail.value; 72 | if (eventValue) { 73 | this._fieldName = eventValue; 74 | this.setCurrentField(); 75 | this.dispatchChangeEvent({ 76 | id: this.expressionId, 77 | fieldName: eventValue 78 | }); 79 | } 80 | } 81 | 82 | setCurrentField() { 83 | if (this._fields && this._fields.length && this._fieldName) { 84 | if (!this.currentField || this.currentField.value !== this._fieldName) { 85 | this.currentField = this._fields.find(curField => curField.value === this._fieldName); 86 | } 87 | } 88 | } 89 | 90 | get disabledFilter() { 91 | return !this._fieldName; 92 | } 93 | 94 | handleOperatorChange(event) { 95 | this.dispatchChangeEvent({ 96 | id: this.expressionId, 97 | operator: event.detail.value 98 | }); 99 | } 100 | 101 | handleValueChange(event) { 102 | this.dispatchChangeEvent({ 103 | id: this.expressionId, 104 | parameter: event.target.value 105 | }); 106 | } 107 | 108 | dispatchChangeEvent(customParams) { 109 | const memberRefreshedEvt = new CustomEvent('fieldselected', { 110 | bubbles: true, 111 | detail: {...this.currentField, ...customParams} 112 | }); 113 | this.dispatchEvent(memberRefreshedEvt); 114 | } 115 | 116 | handleExpressionRemove() { 117 | const expressionRemovedEvent = new CustomEvent('expressionremoved', { 118 | bubbles: true, detail: this.expressionId 119 | }); 120 | this.dispatchEvent(expressionRemovedEvent); 121 | } 122 | 123 | get position() { 124 | return this.expressionIndex + 1; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/expressionLine/expressionLine.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | expressionLine 5 | true 6 | expressionLine 7 | 8 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/formulaBuilder/formulaBuilder.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: var(--lwc-spacingMedium); 3 | border-radius: var(--lwc-borderRadiusMedium); 4 | position: relative; 5 | } 6 | .flexContainer { 7 | display: flex; 8 | padding-bottom: var(--lwc-spacingMedium); 9 | align-items: center; 10 | } 11 | .flexContainer p { 12 | padding-right: var(--lwc-spacingXSmall); 13 | } 14 | lightning-combobox { 15 | padding-right: var(--lwc-spacingXSmall); 16 | } 17 | lightning-button { 18 | margin-left: 0; 19 | } 20 | .resultContainer { 21 | display: flex; 22 | padding-top: var(--lwc-spacingXSmall); 23 | padding-bottom: var(--lwc-spacingXSmall); 24 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/formulaBuilder/formulaBuilder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/formulaBuilder/formulaBuilder.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, track, api, wire} from 'lwc'; 2 | import {FlowAttributeChangeEvent} from 'lightning/flowSupport'; 3 | import describeSObjects from '@salesforce/apex/SearchUtils.describeSObjects'; 4 | 5 | 6 | export default class FormulaBuilder extends LightningElement { 7 | 8 | @api functions; 9 | @api operators; 10 | @api supportedSystemTypes; 11 | @api contextDataString; 12 | 13 | @track formula = ''; 14 | @track _objectName; 15 | @track contextFields = []; 16 | @track contextTypes; 17 | 18 | @track functionValue = null; 19 | 20 | @api get contextObjectType() { 21 | return this._objectName; 22 | } 23 | 24 | set contextObjectType(value) { 25 | this._objectName = value; 26 | if (value) { 27 | this.contextTypes = [...[value], ...this.supportedSystemTypes ? this.splitValues(this.supportedSystemTypes) : []]; 28 | } 29 | } 30 | 31 | @api supportedFunctions = [ 32 | 'AND', 'OR', 'NOT', 'XOR', 'IF', 'CASE', 'LEN', 'SUBSTRING', 'LEFT', 'RIGHT', 33 | 'ISBLANK', 'ISPICKVAL', 'CONVERTID', 'ABS', 'ROUND', 'CEILING', 'FLOOR', 'SQRT', 'ACOS', 34 | 'ASIN', 'ATAN', 'COS', 'SIN', 'TAN', 'COSH', 'SINH', 'TANH', 'EXP', 'LOG', 'LOG10', 'RINT', 35 | 'SIGNUM', 'INTEGER', 'POW', 'MAX', 'MIN', 'MOD', 'TEXT', 'DATETIME', 'DECIMAL', 'BOOLEAN', 36 | 'DATE', 'DAY', 'MONTH', 'YEAR', 'HOURS', 'MINUTES', 'SECONDS', 'ADDDAYS', 'ADDMONTHS', 37 | 'ADDYEARS', 'ADDHOURS', 'ADDMINUTES', 'ADDSECONDS', 'CONTAINS', 'FIND', 'LOWER', 'UPPER' 38 | , 'MID', 'SUBSTITUTE', 'TRIM', 'VALUE', 'CONCATENATE', 'TODAY=>$TODAY', 'WEEKDAY', 'BEGINS' 39 | ]; 40 | 41 | @api supportedOperators = ['+', '-', '/', '*', '==', '!=', '>', '<', '>=', '<=', '<>']; 42 | 43 | @api 44 | get formulaString() { 45 | return this.formula; 46 | } 47 | 48 | set formulaString(value) { 49 | this.formula = value; 50 | } 51 | 52 | @wire(describeSObjects, {types: '$contextTypes'}) 53 | _describeSObjects(result) { 54 | if (result.error) { 55 | console.log(result.error.body.message); 56 | // this.errors.push(error.body[0].message); 57 | } else if (result.data) { 58 | this.contextTypes.forEach(objType => { 59 | 60 | let newContextFields = result.data[objType].map(curField => { 61 | return {label: objType + ': ' + curField.label, value: '$' + objType + '.' + curField.value} 62 | }); 63 | 64 | if (this.contextFields) { 65 | this.contextFields = this.contextFields.concat(newContextFields); 66 | } else { 67 | this.contextFields = newContextFields; 68 | } 69 | 70 | }); 71 | if (this.contextDataString) { 72 | let contextDataObj = JSON.parse(this.contextDataString); 73 | contextDataObj.forEach(curEl => { 74 | this.contextFields.push({label: 'Context Data: ' + curEl.name, value: curEl.value}); 75 | }) 76 | } 77 | } 78 | } 79 | 80 | formulaChangedEvent() { 81 | const memberRefreshedEvt = new CustomEvent('formulachanged', { 82 | bubbles: true, detail: { 83 | value: this.formula 84 | } 85 | }); 86 | this.dispatchEvent(memberRefreshedEvt); 87 | } 88 | 89 | dispatchFormulaChangedEvents() { 90 | this.formulaChangedFlowEvent(); 91 | this.formulaChangedEvent(); 92 | } 93 | 94 | setFunctions() { 95 | let functions = []; 96 | this.supportedFunctions.sort((a, b) => (a > b) ? 1 : ((b > a) ? -1 : 0)).forEach(func => { 97 | let funcParts = func.split('=>'); 98 | functions.push({label: funcParts[0], value: funcParts.length == 1 ? funcParts[0] + '()' : funcParts[1]}); 99 | }); 100 | this.functions = functions; 101 | } 102 | 103 | connectedCallback() { 104 | 105 | this.setFunctions(); 106 | let operators = []; 107 | 108 | this.supportedOperators.forEach(operator => { 109 | operators.push({label: operator, value: operator}); 110 | }); 111 | 112 | this.operators = operators; 113 | 114 | } 115 | 116 | formulaChangedFlowEvent(event) { 117 | const valueChangeEvent = new FlowAttributeChangeEvent('value', this.formula); 118 | this.dispatchEvent(valueChangeEvent); 119 | } 120 | 121 | insertInCurrentPosition(value, setCursor) { 122 | let formulaTextArea = this.template.querySelector('.slds-textarea'); 123 | if (formulaTextArea) { 124 | if (value !== '') { 125 | let formulaStart = this.formula.substring(0, formulaTextArea.selectionStart) + ' ' + value + ' '; 126 | let newFormulaString = formulaStart + this.formula.substring(formulaTextArea.selectionEnd); 127 | this.formula = newFormulaString; 128 | formulaTextArea.value = newFormulaString; 129 | if (setCursor) { 130 | formulaTextArea.focus(); 131 | formulaTextArea.selectionEnd = formulaStart.length - 2; 132 | } 133 | this.dispatchFormulaChangedEvents(); 134 | } 135 | } 136 | 137 | } 138 | 139 | selectOperator(event) { 140 | this.insertInCurrentPosition(event.detail.value); 141 | this.clearSelections(); 142 | } 143 | 144 | selectFunction(event) { 145 | this.insertInCurrentPosition(event.detail.value, true); 146 | this.clearSelections(); 147 | } 148 | 149 | clearSelections() { 150 | this.template.querySelectorAll('lightning-combobox').forEach(curComboBox => { 151 | curComboBox.value = null; 152 | }); 153 | } 154 | 155 | changeFormula(event) { 156 | this.formula = event.target.value; 157 | this.dispatchFormulaChangedEvents(); 158 | } 159 | 160 | selectField(event) { 161 | this.insertInCurrentPosition(event.detail.value, false); 162 | this.clearSelections(); 163 | } 164 | 165 | splitValues(originalString) { 166 | return originalString ? originalString.replace(/ /g, '').split(',') : []; 167 | } 168 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/formulaBuilder/formulaBuilder.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | true 5 | 6 | lightning__AppPage 7 | lightning__RecordPage 8 | lightning__HomePage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "c/buttonUtils": [ 6 | "buttonUtils/buttonUtils.js" 7 | ], 8 | "c/expressionBuilder": [ 9 | "expressionBuilder/expressionBuilder.js" 10 | ], 11 | "c/expressionLine": [ 12 | "expressionLine/expressionLine.js" 13 | ], 14 | "c/formulaBuilder": [ 15 | "formulaBuilder/formulaBuilder.js" 16 | ], 17 | "c/lwcLogger": [ 18 | "lwcLogger/lwcLogger.js" 19 | ], 20 | "c/pickObjectAndFieldFSC": [ 21 | "pickObjectAndFieldFSC/pickObjectAndFieldFSC.js" 22 | ], 23 | "c/setOwner": [ 24 | "setOwner/setOwner.js" 25 | ], 26 | "c/setPickList": [ 27 | "setPickList/setPickList.js" 28 | ], 29 | "c/updateFieldCPE": [ 30 | "updateFieldCPE/updateFieldCPE.js" 31 | ] 32 | }, 33 | "experimentalDecorators": true 34 | }, 35 | "include": [ 36 | "**/*", 37 | "../../../../.sfdx/typings/lwc/**/*.d.ts" 38 | ], 39 | "typeAcquisition": { 40 | "include": [ 41 | "jest" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/lwcLogger/lwcLogger.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | //WARNING inverted logOn because I can't figure out how to efficiently turn on logging 4 | 5 | /* eslint-disable no-console */ 6 | const logger = ( 7 | logOn = false, 8 | source = 'unspecified source', 9 | message, 10 | data, 11 | ) => { 12 | if (!logOn) { 13 | try { 14 | if (data) { 15 | console.log( 16 | `${source}: ${message}`, 17 | JSON.parse(JSON.stringify(data)), 18 | ); 19 | } else { 20 | console.log(`${source}: ${message}`); 21 | } 22 | } catch (e) { 23 | if (data) { 24 | console.log(`${source}: ${message}`, data); 25 | } else { 26 | console.log(`${source}: ${message}`); 27 | } 28 | } 29 | } 30 | }; 31 | 32 | const logError = ( 33 | logOn = false, 34 | source = 'unspecified source', 35 | message, 36 | data, 37 | ) => { 38 | if (!logOn) { 39 | try { 40 | if (data) { 41 | console.log( 42 | `${source}: ${message}`, 43 | JSON.parse(JSON.stringify(data)), 44 | ); 45 | } else { 46 | console.log(`${source}: ${message}`); 47 | } 48 | } catch (e) { 49 | if (data) { 50 | console.log(`${source}: ${message}`, data); 51 | } else { 52 | console.log(`${source}: ${message}`); 53 | } 54 | } 55 | } 56 | }; 57 | 58 | export { logger, logError }; 59 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/lwcLogger/lwcLogger.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.css: -------------------------------------------------------------------------------- 1 | .spinner-holder{ 2 | width: 80px; 3 | height: 80px; 4 | } 5 | .field-picker{ 6 | position: relative; 7 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Pick Object And Field FSC 5 | true 6 | Pick Object And Field FSC 7 | 8 | lightning__FlowScreen 9 | lightning__RecordPage 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/setOwner/setOwner.css: -------------------------------------------------------------------------------- 1 | .button-align-bottom { 2 | align-self: flex-end; 3 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/setOwner/setOwner.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/setOwner/setOwner.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track, wire} from 'lwc'; 2 | 3 | import Search from '@salesforce/label/c.Search'; 4 | import For from '@salesforce/label/c.For'; 5 | import TooManyResultsMessage from '@salesforce/label/c.TooManyResultsMessage'; 6 | import Queues from '@salesforce/label/c.Queues'; 7 | import RelatedUsers from '@salesforce/label/c.RelatedUsers'; 8 | import PublicGroups from '@salesforce/label/c.PublicGroups'; 9 | import Roles from '@salesforce/label/c.Roles'; 10 | import Users from '@salesforce/label/c.Users'; 11 | 12 | import USER_NAME_FIELD from '@salesforce/schema/User.Name'; 13 | import GROUP_NAME_FIELD from '@salesforce/schema/Group.Name'; 14 | 15 | import searchMemberByType from '@salesforce/apex/SearchUtils.searchMemberByType'; 16 | 17 | import getSingleMembersByTypeAndId from '@salesforce/apex/SearchUtils.getSingleMembersByTypeAndId'; 18 | import {ShowToastEvent} from 'lightning/platformShowToastEvent'; 19 | import {logger, logError} from 'c/lwcLogger'; 20 | 21 | import { 22 | generateCapabilityColumns, 23 | splitValues 24 | } from 'c/buttonUtils'; 25 | 26 | 27 | const typeMapping = { 28 | Group: PublicGroups, 29 | Role: Roles, 30 | User: Users, 31 | Queue: Queues, 32 | RelatedUsers: RelatedUsers 33 | }; 34 | 35 | export default class addNewMembers extends LightningElement { 36 | @api notifyAssignee = false; 37 | @api memberId; 38 | @api availableObjectTypes; 39 | @api supportedAddCapabilities; 40 | @api notifyAssigneeLabel; 41 | 42 | @track memberData; 43 | @track showMemberSelect = false; 44 | @track label = { 45 | Search, 46 | TooManyResultsMessage, 47 | For 48 | }; 49 | @track searchString = ''; 50 | @track selectedType; 51 | @track searchResults = []; 52 | @track searchDisabled = false; 53 | source = 'ownerSetter'; 54 | 55 | @wire(getSingleMembersByTypeAndId, {type: '$selectedType', id: '$memberId'}) 56 | _getSingleMembersByTypeAndId({error, data}) { 57 | if (error) { 58 | console.log(error.body.message); 59 | } else if (data) { 60 | this.memberData = data[this.memberId]; 61 | } 62 | } 63 | 64 | connectedCallback() { 65 | if (this.availableObjectTypes && this.availableObjectTypes.length > 0) { 66 | this.selectedType = splitValues(this.availableObjectTypes)[0]; 67 | } 68 | } 69 | 70 | get objectTypes() { 71 | return splitValues(this.availableObjectTypes).map(curTypeName => { 72 | return this.getTypeDescriptor(curTypeName); 73 | }); 74 | } 75 | 76 | getTypeDescriptor(typeName) { 77 | return {value: typeName, label: typeMapping[typeName]}; 78 | } 79 | 80 | get tooManyResults() { 81 | return this.searchResults.length > 199; 82 | } 83 | 84 | get columns() { 85 | return [{ 86 | label: 'Name', 87 | fieldName: 'label' 88 | }].concat(generateCapabilityColumns(this.supportedAddCapabilities)); 89 | } 90 | 91 | get showNotifyAssignee() { 92 | return this.selectedType === 'User'; 93 | } 94 | 95 | get selectedMemberName() { 96 | if (this.memberData) { 97 | return this.memberData.Name; 98 | } else { 99 | return null; 100 | } 101 | } 102 | 103 | handleTypeChange(event) { 104 | this.selectedType = event.detail.value; 105 | this.searchResults = []; 106 | } 107 | 108 | async actuallySearch() { 109 | this.searchResults = []; 110 | this.searchDisabled = true; 111 | 112 | const results = 113 | await searchMemberByType({ 114 | searchString: this.searchString, 115 | memberTypes: [this.selectedType] 116 | }); 117 | this.searchResults = results[this.selectedType]; 118 | 119 | this.searchDisabled = false; 120 | } 121 | 122 | searchEventHandler(event) { 123 | if (event.detail.value) { 124 | const searchString = event.detail.value 125 | .trim() 126 | .replace(/\*/g) 127 | .toLowerCase(); 128 | 129 | if (searchString.length <= 1) { 130 | return; 131 | } 132 | 133 | this.searchString = searchString; 134 | } 135 | } 136 | 137 | listenForEnter(event) { 138 | if (event.code === 'Enter') { 139 | this.actuallySearch(); 140 | } 141 | } 142 | 143 | handleRowAction(event) { 144 | const memberRefreshedEvt = new CustomEvent('membersrefreshed', { 145 | bubbles: true, detail: { 146 | memberId: event.detail.row.value, 147 | notifyAssignee: (this.selectedType === 'User' ? this.notifyAssignee : false), 148 | } 149 | }); 150 | this.dispatchEvent(memberRefreshedEvt); 151 | } 152 | 153 | toastTheError(e, errorSource) { 154 | logError(this.log, this.source, errorSource, e); 155 | this.dispatchEvent( 156 | new ShowToastEvent({ 157 | message: e.body.message, 158 | variant: 'error' 159 | }) 160 | ); 161 | } 162 | 163 | toggleMemberSelect() { 164 | this.showMemberSelect = !this.showMemberSelect; 165 | } 166 | 167 | handleNotifyAssigneeChange(event) { 168 | this.notifyAssignee = event.target.checked === true; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/setOwner/setOwner.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | true 5 | 6 | lightning__RecordPage 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/setPickList/setPickList.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/setPickList/setPickList.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track, wire} from 'lwc'; 2 | import getPicklistValues from '@salesforce/apex/FieldPickerController.getPicklistValues'; 3 | import NonePicklistValueLabel from '@salesforce/label/c.NonePicklistValueLabel'; 4 | 5 | export default class setPickList extends LightningElement { 6 | @api masterLabel = 'Picklist Options'; 7 | @api valueAboveRadioLabel = 'The value above the current one'; 8 | @api valueBelowRadioLabel = 'The value below the current one'; 9 | @api specificValueRadioLabel = 'A Specific Value'; 10 | @api picklistObjectName; 11 | @api picklistFieldName; 12 | @api selectionType; 13 | @api value; 14 | 15 | @track _selectionType; 16 | @track _value; 17 | @track availableValues; 18 | @track errors = []; 19 | 20 | labels = { 21 | none: NonePicklistValueLabel, 22 | previous: '__PicklistPrevious', 23 | next: '__PicklistNext', 24 | nullValue: '__null' 25 | }; 26 | 27 | connectedCallback() { 28 | // this._selectionType = this.selectionType; 29 | this._value = (this.value === null ? this.labels.nullValue : this.value); 30 | if (this._value === this.labels.next || this._value === this.labels.previous) { 31 | this._selectionType = this._value; 32 | } else { 33 | this._selectionType = this.labels.nullValue; 34 | } 35 | } 36 | 37 | @wire(getPicklistValues, {objectApiName: '$picklistObjectName', fieldName: '$picklistFieldName'}) 38 | _getPicklistValues({error, data}) { 39 | if (error) { 40 | this.errors.push(error.body.message); 41 | } else if (data) { 42 | this.availableValues = JSON.parse(JSON.stringify(data)); 43 | this.availableValues.unshift({value: this.labels.nullValue, label: this.labels.none}); 44 | } 45 | } 46 | 47 | get picklistOptions() { 48 | return [ 49 | {label: this.valueAboveRadioLabel, value: this.labels.previous}, 50 | {label: this.valueBelowRadioLabel, value: this.labels.next}, 51 | {label: this.specificValueRadioLabel, value: this.labels.nullValue}]; 52 | } 53 | 54 | get isSpecificValue() { 55 | return this._selectionType === this.labels.nullValue; 56 | } 57 | 58 | handleOptionChange(event) { 59 | this._selectionType = event.detail.value; 60 | this.handlePicklistValueChange(event); 61 | } 62 | 63 | handlePicklistValueChange(event) { 64 | const memberRefreshedEvt = new CustomEvent('picklistselected', { 65 | bubbles: true, detail: { 66 | value: (event.detail.value === this.labels.nullValue ? null : event.detail.value), 67 | selectionType: this._selectionType 68 | } 69 | }); 70 | this.dispatchEvent(memberRefreshedEvt); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/setPickList/setPickList.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | setPickList 5 | true 6 | setPickList 7 | 8 | -------------------------------------------------------------------------------- /force-app/main/default/lwc/updateFieldCPE/updateFieldCPE.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, wire, track} from 'lwc'; 2 | 3 | export default class updateFieldConfigurator extends LightningElement { 4 | 5 | @api supportedSystemTypes; 6 | 7 | @api values; 8 | @api property; 9 | @api flowContext; 10 | @api validate(){} 11 | 12 | @track _value; 13 | @track _objectType; 14 | @track _fieldName; 15 | @track selectedField; 16 | @track textOption; 17 | @track formulaEditorVisible = false; 18 | @track formulaEditorMessage = 'Show Formula Editor'; 19 | 20 | labels = { 21 | fieldTypeNotSupported: 'Selected field type is not supported', 22 | fieldValueLabel: 'Set Field Value', 23 | fieldNotUpdatable: 'Select field can not be updated' 24 | }; 25 | 26 | customReferenceTypes = ['User']; 27 | 28 | @api get objectType() { 29 | return this._objectType; 30 | } 31 | 32 | set objectType(value) { 33 | this._objectType = value; 34 | } 35 | 36 | @api get fieldName() { 37 | return this._fieldName; 38 | } 39 | 40 | set fieldName(value) { 41 | this._fieldName = value; 42 | } 43 | 44 | @api get value() { 45 | return this._value; 46 | } 47 | 48 | set value(value) { 49 | this._value = value; 50 | } 51 | 52 | get textOptions() { 53 | let resultTextOptions = []; 54 | if (this.fieldProperties && !this.fieldProperties.isRequired) { 55 | resultTextOptions.push({label: 'A blank value (null)', value: 'null'}); 56 | } 57 | resultTextOptions.push({label: 'Use a formula to set the new value', value: 'formula_builder'}); 58 | return resultTextOptions; 59 | } 60 | 61 | get checkboxOptions() { 62 | return [ 63 | {label: 'True', value: 'true'}, 64 | {label: 'False', value: 'false'}]; 65 | } 66 | 67 | handleFieldChange(event) { 68 | this.selectedField = JSON.parse(JSON.stringify(event.detail)); 69 | if (this._objectType !== this.selectedField.objectType) { 70 | this._objectType = this.selectedField.objectType; 71 | } 72 | if (this._fieldName !== this.selectedField.fieldName) { 73 | this._fieldName = this.selectedField.fieldName; 74 | } 75 | if (!this.selectedField.isInit) { 76 | this._value = null; 77 | } 78 | } 79 | 80 | handleValueChange(event) { 81 | this._value = event.detail.value; 82 | } 83 | 84 | handleOwnerChange(event) { 85 | this._value = event.detail.memberId; 86 | // event.detail.notifyAssignee; 87 | } 88 | 89 | handleTextOptionValueChange(event) { 90 | this.textOption = event.detail.value; 91 | } 92 | 93 | handleSave(event) { 94 | 95 | } 96 | 97 | toggleFormulaEditor() { 98 | this.formulaEditorVisible = !this.formulaEditorVisible; 99 | if (this.formulaEditorVisible) { 100 | this.formulaEditorMessage = 'Hide Formula Editor'; 101 | } else { 102 | this.formulaEditorMessage = 'Show Formula Editor' 103 | } 104 | } 105 | 106 | get showFormulaBuilderOption() { 107 | return this.textOption === 'formula_builder'; 108 | } 109 | 110 | get fieldProperties() { 111 | if (this.selectedField && this.selectedField.fieldName) { 112 | return { 113 | ...this.selectedField, ...{ 114 | isTextField: this.selectedField.dataType === 'String' || (this.selectedField.dataType === 'Reference' && !this.customReferenceTypes.some(refType => this.selectedField.referenceTo.includes(refType))), 115 | isUserReferenceField: this.selectedField.referenceTo.includes('User'), 116 | isBoolean: this.selectedField.dataType === 'Boolean', 117 | isPicklist: this.selectedField.dataType === 'Picklist', 118 | isDateTime: this.selectedField.dataType === 'DateTime', 119 | isDate: this.selectedField.dataType === 'Date', 120 | isCurrency: this.selectedField.dataType === 'Currency', 121 | isAddress: this.selectedField.dataType === 'Address', 122 | isDouble: this.selectedField.dataType === 'Double' || this.selectedField.dataType === 'Int', 123 | isTextArea: this.selectedField.dataType === 'TextArea', 124 | isPhone: this.selectedField.dataType === 'Phone', 125 | isUrl: this.selectedField.dataType === 'Url', 126 | isDisabled: this.selectedField.updateable !== true, 127 | isRequired: this.selectedField.required === true 128 | } 129 | } 130 | } 131 | return null; 132 | } 133 | } -------------------------------------------------------------------------------- /force-app/main/default/lwc/updateFieldCPE/updateFieldCPE.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | updateFieldConfigurator 5 | true 6 | updateFieldConfigurator 7 | 8 | lightning__RecordPage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/zip 5 | Static resource for sites sample pages 6 | 7 | -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/SiteStyles.css: -------------------------------------------------------------------------------- 1 | .topPanelContainer { 2 | text-align:left; 3 | border:1px solid #ccc; 4 | } 5 | 6 | .topPanel { 7 | background-color: white; 8 | border: 1px solid #ccc; 9 | padding: 0px; 10 | margin-top: 10px; 11 | margin-bottom: 0px; 12 | margin-left: 10px; 13 | margin-right: 10px; 14 | } 15 | 16 | .title { 17 | font-size: larger; 18 | font-weight: bold; 19 | } 20 | 21 | .poweredByImage { 22 | vertical-align: middle; 23 | margin:12px 8px 8px 0; 24 | } 25 | 26 | img { 27 | border: none; 28 | } -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/clock.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/construction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/construction.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/force_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/force_logo.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/maintenance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/maintenance.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/poweredby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/poweredby.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/tools.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/unauthorized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/unauthorized.png -------------------------------------------------------------------------------- /force-app/main/default/staticresources/SiteSamples/img/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/force-app/main/default/staticresources/SiteSamples/img/warning.png -------------------------------------------------------------------------------- /mdapi/classes/AssembleInputParams.cls: -------------------------------------------------------------------------------- 1 | 2 | public with sharing class AssembleInputParams { 3 | private static final Integer NUMBER_OF_PARAMS = 4; 4 | 5 | @InvocableMethod 6 | public static List execute(List requestList) { 7 | System.debug('entering AssembleInputParams'); 8 | Map requestListMap = (Map) JSON.deserialize(JSON.serialize(requestList[0]), Map.class); 9 | 10 | //Create a Results object to hold the return values 11 | Results response = new Results(); 12 | List params = new List(); 13 | for (Integer i = 1; i <= NUMBER_OF_PARAMS; i++) { 14 | String paramKey = (String) requestListMap.get('param' + i + 'Key'); 15 | String paramValue = (String) requestListMap.get('param' + i + 'Value'); 16 | if ((paramKey != null && paramValue == null) || (paramKey == null && paramValue != null)) 17 | throw new InvocableActionException('You need to provide both a param1Key and a param1Value. Currently you are only providing 1 of those'); 18 | if (paramKey != null && paramValue != null) { 19 | params.add(new ContextWrapper(paramKey, paramValue)); 20 | } 21 | } 22 | 23 | response.jsonParams = JSON.serialize(params); 24 | 25 | //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) 26 | List responseWrapper = new List(); 27 | responseWrapper.add(response); 28 | System.debug('response is: ' + response); 29 | System.debug('responseWrapper is: ' + responseWrapper); 30 | 31 | return responseWrapper; 32 | 33 | } 34 | 35 | public class InvocableActionException extends Exception { 36 | } 37 | 38 | 39 | public class Requests { 40 | 41 | @InvocableVariable 42 | public String param1Key; 43 | 44 | @InvocableVariable 45 | public String param1Value; 46 | 47 | @InvocableVariable 48 | public String param2Key; 49 | 50 | @InvocableVariable 51 | public String param2Value; 52 | 53 | @InvocableVariable 54 | public String param3Key; 55 | 56 | @InvocableVariable 57 | public String param3Value; 58 | 59 | @InvocableVariable 60 | public String param4Key; 61 | 62 | @InvocableVariable 63 | public String param4Value; 64 | 65 | } 66 | 67 | public class Results { 68 | 69 | @InvocableVariable 70 | public String jsonParams; 71 | 72 | } 73 | 74 | public class ContextWrapper { 75 | public String name; 76 | public String value; 77 | 78 | public ContextWrapper(String name, String value) { 79 | this.name = name; 80 | this.value = value; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /mdapi/classes/AssembleInputParams.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/EvaluateFormula.cls: -------------------------------------------------------------------------------- 1 | public with sharing class EvaluateFormula { 2 | 3 | @InvocableMethod 4 | public static List evaluate(List requestList) { 5 | System.debug('entering Evaluate Formula'); 6 | 7 | String formulaString = requestList[0].formulaString; 8 | String recordId = requestList[0].recordId; 9 | String contextDataString = JSON.serialize(generateContextByRecordId(recordId)); 10 | 11 | String result = FormulaEvaluator.parseFormula(formulaString, contextDataString); 12 | //TO DO: need to fix this to return non-integer values effectively. we may need to pass in an indicator as to what 13 | //what the input type is 14 | 15 | //Create a Results object to hold the return values 16 | Results response = new Results(); 17 | response.stringResult = result; 18 | 19 | //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification) 20 | List responseWrapper = new List(); 21 | responseWrapper.add(response); 22 | System.debug('response is: ' + response); 23 | System.debug('responseWrapper is: ' + responseWrapper); 24 | 25 | return responseWrapper; 26 | 27 | } 28 | 29 | private static List generateContextByRecordId(String recordId) { 30 | List context = new List(); 31 | if(!String.isBlank(recordId)){ 32 | context.add(new FormulaEvaluator.ContextWrapper('$Record', recordId)); 33 | } 34 | return context; 35 | } 36 | 37 | public class Requests { 38 | 39 | @InvocableVariable 40 | public String formulaString; 41 | 42 | @InvocableVariable 43 | public String recordId; 44 | 45 | } 46 | 47 | public class Results { 48 | 49 | 50 | @InvocableVariable 51 | public String stringResult; 52 | 53 | @InvocableVariable 54 | public Decimal numberResult; 55 | 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /mdapi/classes/EvaluateFormula.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/ExpressionBuilder.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/ExpressionBuilderTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class ExpressionBuilderTest { 3 | @IsTest 4 | static void assembleAndDisassemblyFormulaStringTest() { 5 | String expression = '1 AND (2 OR 3) AND 4 AND (5 OR 6 OR (7 AND 8)) AND (1 OR 2)'; 6 | String testData = '[{"objectType":"Account","fieldName":"$Account.AccountNumber","dataType":"String","operator":"starts_with","parameter":"1"}, ' + 7 | '{"objectType":"Account","fieldName":"$Account.AccountSource","dataType":"String","operator":"not_equal_to","parameter":"A"}, ' + 8 | '{"objectType":"Account","fieldName":"$Account.AnnualRevenue","dataType":"Currency","operator":"less_then","parameter":"2"}, ' + 9 | '{"objectType":"Account","fieldName":"$Account.Id","dataType":"String","operator":"equals","parameter":"3"}, ' + 10 | '{"objectType":"User","fieldName":"$User.AccountNumber","dataType":"String","operator":"contains","parameter":"1"},' + 11 | '{"objectType":"Organization","fieldName":"$Organization.AccountSource","dataType":"String","operator":"does_not_contain","parameter":"A"}, ' + 12 | '{"objectType":"Account","fieldName":"$Account.AnnualRevenue","dataType":"Currency","operator":"includes","parameter":"2"},' + 13 | '{"objectType":"Account","fieldName":"$Account.Id","dataType":"String","operator":"excludes","parameter":"3"}]'; 14 | 15 | String assembleResult = ExpressionBuilder.assembleFormulaString(expression, 'CUSTOM', testData); 16 | String expectResult = 'AND(LEFT($Account.AccountNumber, 1) == TEXT("1"), OR($Account.AccountSource != TEXT("A"), ' + 17 | '$Account.AnnualRevenue < DECIMAL("2")), $Account.Id == TEXT("3"), OR(CONTAINS($User.AccountNumber, TEXT("1")), ' + 18 | '!CONTAINS($Organization.AccountSource, TEXT("A")), AND(INCLUDES($Account.AnnualRevenue, DECIMAL("2")), ' + 19 | '!INCLUDES($Account.Id, TEXT("3")))), OR(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A")))'; 20 | 21 | System.assertEquals(expectResult, assembleResult); 22 | 23 | Map disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 24 | List data = (List)disassemblyResult.get('expressionLines'); 25 | 26 | System.assertEquals('CUSTOM', String.valueOf(disassemblyResult.get('logicType'))); 27 | //Switching off custom logic asserts due to not stable work 28 | //System.assertEquals(expression, String.valueOf(disassemblyResult.get('customLogic'))); 29 | System.assertEquals(8, data.size()); 30 | 31 | 32 | assembleResult = ExpressionBuilder.assembleFormulaString('', 'AND', testData); 33 | expectResult = 'AND(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A"), ' + 34 | '$Account.AnnualRevenue < DECIMAL("2"), $Account.Id == TEXT("3"), CONTAINS($User.AccountNumber, TEXT("1")), ' + 35 | '!CONTAINS($Organization.AccountSource, TEXT("A")), INCLUDES($Account.AnnualRevenue, DECIMAL("2")), !INCLUDES($Account.Id, TEXT("3")))'; 36 | 37 | System.assertEquals(expectResult, assembleResult); 38 | 39 | disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 40 | data = (List)disassemblyResult.get('expressionLines'); 41 | 42 | System.assertEquals('AND', String.valueOf(disassemblyResult.get('logicType'))); 43 | System.assertEquals('', String.valueOf(disassemblyResult.get('customLogic'))); 44 | System.assertEquals(8, data.size()); 45 | 46 | assembleResult = ExpressionBuilder.assembleFormulaString('', 'OR', testData); 47 | expectResult = 'OR(LEFT($Account.AccountNumber, 1) == TEXT("1"), $Account.AccountSource != TEXT("A"), ' + 48 | '$Account.AnnualRevenue < DECIMAL("2"), $Account.Id == TEXT("3"), CONTAINS($User.AccountNumber, TEXT("1")), ' + 49 | '!CONTAINS($Organization.AccountSource, TEXT("A")), INCLUDES($Account.AnnualRevenue, DECIMAL("2")), !INCLUDES($Account.Id, TEXT("3")))'; 50 | 51 | System.assertEquals(expectResult, assembleResult); 52 | 53 | disassemblyResult = ExpressionBuilder.disassemblyFormulaString(assembleResult); 54 | data = (List)disassemblyResult.get('expressionLines'); 55 | 56 | System.assertEquals('OR', String.valueOf(disassemblyResult.get('logicType'))); 57 | System.assertEquals('', String.valueOf(disassemblyResult.get('customLogic'))); 58 | System.assertEquals(8, data.size()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mdapi/classes/ExpressionBuilderTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FieldPickerController.cls: -------------------------------------------------------------------------------- 1 | global with sharing class FieldPickerController { 2 | 3 | @AuraEnabled(cacheable=true) 4 | public static List getObjects(List availableObjectTypes) { 5 | 6 | List result = new List(); 7 | Map allTypes = new Map(); 8 | 9 | if (availableObjectTypes == null || availableObjectTypes.size() == 0) { 10 | allTypes = Schema.getGlobalDescribe(); 11 | availableObjectTypes = new List(allTypes.keySet()); 12 | } else if (availableObjectTypes.size() > 0) { 13 | for (String curType : availableObjectTypes) { 14 | allTypes.put(curType, ((SObject) (Type.forName('Schema.' + curType).newInstance())).getSObjectType()); 15 | } 16 | } 17 | 18 | for (String objType : availableObjectTypes) { 19 | 20 | Schema.DescribeSObjectResult describeObject = allTypes.get(objType).getDescribe(); 21 | String objectType = describeObject.getName(); 22 | String objectLabel = describeObject.getLabel(); 23 | Boolean isCustom = describeObject.isCustom(); 24 | 25 | if (isCustom || allowedStandardObjects.containsKey(objectType)) { 26 | result.add(new Member(objectType, objectLabel)); 27 | } 28 | 29 | result.sort(); 30 | 31 | } 32 | 33 | return result; 34 | 35 | } 36 | @AuraEnabled(cacheable=true) 37 | public static List getPicklistValues(String objectApiName, String fieldName) { 38 | 39 | List options = new List(); 40 | 41 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(objectApiName).getDescribe(); 42 | Map fieldMap = describeObject.fields.getMap(); 43 | List values = fieldMap.get(fieldName).getDescribe().getPickListValues(); 44 | 45 | for (Schema.PicklistEntry a : values) { 46 | options.add(new Member(a.getValue(), a.getLabel())); 47 | } 48 | 49 | options.sort(); 50 | 51 | return options; 52 | } 53 | 54 | 55 | public class Member implements Comparable { 56 | 57 | @AuraEnabled 58 | public String label; 59 | @AuraEnabled 60 | public String value; 61 | 62 | public Member(String value, String label) { 63 | this.label = label; 64 | this.value = value; 65 | } 66 | 67 | public Integer compareTo(Object compareTo) { 68 | 69 | Member curMember = (Member) compareTo; 70 | 71 | if (label == curMember.label) { 72 | return 0; 73 | } 74 | if (label > curMember.label) { 75 | return 1; 76 | } 77 | return -1; 78 | 79 | } 80 | 81 | } 82 | @TestVisible 83 | private static Map allowedStandardObjects = new Map{ 84 | 'Account' => 'Account', 85 | 'AccountPartner' => 'Account Partner', 86 | 'Asset' => 'Asset', 87 | 'AssetRelationship' => 'Asset Relationship', 88 | 'AssignedResource' => 'Assigned Resource', 89 | 'Campaign' => 'Campaign', 90 | 'CampaignMember' => 'Campaign Member', 91 | 'Case' => 'Case', 92 | 'Contact' => 'Contact', 93 | 'ContactRequest' => 'Contact Request', 94 | 'ContentDocument' => 'File', 95 | 'ContentVersion' => 'File', 96 | 'ContentWorkspace' => 'Library', 97 | 'Contract' => 'Contract', 98 | 'ContractContactRole' => 'Contract Contact Role', 99 | 'Image' => 'Image', 100 | 'Individual' => 'Individual', 101 | 'Lead' => 'Lead', 102 | 'MaintenanceAsset' => 'Maintenance Asset', 103 | 'MaintenancePlan' => 'Maintenance Plan', 104 | 'Note' => 'Note', 105 | 'OperatingHours' => 'Operating Hours', 106 | 'Opportunity' => 'Opportunity', 107 | 'OpportunityLineItem' => 'Opportunity Product', 108 | 'OpportunityPartner' => 'Opportunity Partner', 109 | 'Order' => 'Order', 110 | 'OrderItem' => 'Order Product', 111 | 'Partner' => 'Partner', 112 | 'Pricebook2' => 'Price Book', 113 | 'PricebookEntry' => 'Price Book Entry', 114 | 'Product2' => 'Product', 115 | 'RecordType' => 'Record Type', 116 | 'ResourceAbsence' => 'Resource Absence', 117 | 'ResourcePreference' => 'Resource Preference', 118 | 'ReturnOrder' => 'Return Order', 119 | 'ReturnOrderLineItem' => 'Return Order Line Item', 120 | 'ServiceAppointment' => 'Service Appointment', 121 | 'ServiceCrew' => 'Service Crew', 122 | 'ServiceCrewMember' => 'Service Crew Member', 123 | 'ServiceResource' => 'Service Resource', 124 | 'ServiceResourceCapacity' => 'Resource Capacity', 125 | 'ServiceResourceSkill' => 'Service Resource Skill', 126 | 'ServiceTerritory' => 'Service Territory', 127 | 'ServiceTerritoryLocation' => 'Service Territory Location', 128 | 'ServiceTerritoryMember' => 'Service Territory Member', 129 | 'Shift' => 'Shift', 130 | 'Shipment' => 'Shipment', 131 | 'SkillRequirement' => 'Skill Requirement', 132 | 'TimeSheet' => 'Time Sheet', 133 | 'TimeSheetEntry' => 'Time Sheet Entry', 134 | 'TimeSlot' => 'Time Slot', 135 | 'User' => 'User', 136 | 'WorkOrder' => 'Work Order', 137 | 'WorkOrderLineItem' => 'Work Order Line Item', 138 | 'WorkType' => 'Work Type' 139 | }; 140 | } -------------------------------------------------------------------------------- /mdapi/classes/FieldPickerController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FieldPickerControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class FieldPickerControllerTest { 3 | private static final String OBJECT_NAME = 'Account'; 4 | private static final String PICKLIST_FIELD_NAME = 'AccountSource'; 5 | 6 | @IsTest 7 | static void testGetObjects() { 8 | //Get two supported and one not supported object 9 | List members = FieldPickerController.getObjects(new List{ 10 | 'Account', 'User' 11 | }); 12 | System.assertEquals(2, members.size()); 13 | System.assertEquals('Account', members[0].label); 14 | System.assertEquals('User', members[1].label); 15 | //Get all objects 16 | members = FieldPickerController.getObjects(NULL); 17 | for (FieldPickerController.Member m : members) { 18 | System.debug(m.label); 19 | } 20 | //All supported objects plus all custom are different on different types of orgs, so commenting this line out. 21 | //System.assertEquals(true, members.size() >= FieldPickerController.allowedStandardObjects.size()); 22 | } 23 | 24 | @IsTest 25 | static void testGetPicklistValues() { 26 | List members = FieldPickerController.getPicklistValues(OBJECT_NAME, PICKLIST_FIELD_NAME); 27 | 28 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(OBJECT_NAME).getDescribe(); 29 | Map fieldMap = describeObject.fields.getMap(); 30 | List values = fieldMap.get(PICKLIST_FIELD_NAME).getDescribe().getPickListValues(); 31 | 32 | System.assertEquals(members.size(), values.size()); 33 | } 34 | } -------------------------------------------------------------------------------- /mdapi/classes/FieldPickerControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaBuilderController.cls: -------------------------------------------------------------------------------- 1 | public with sharing class FormulaBuilderController { 2 | 3 | @AuraEnabled(cacheable=true) 4 | public static List getFieldList(String objectName) { 5 | List result = new List(); 6 | 7 | Schema.DescribeSObjectResult describeObject = Schema.getGlobalDescribe().get(objectName).getDescribe(); 8 | Map fields = describeObject.fields.getMap(); 9 | 10 | for(Schema.sObjectField field : fields.values()) { 11 | result.add(field.getDescribe().getName()); 12 | } 13 | 14 | return result; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaBuilderController.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaBuilderControllerTest.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public with sharing class FormulaBuilderControllerTest { 3 | @IsTest 4 | static void getFieldListTest() { 5 | List result = FormulaBuilderController.getFieldList('Opportunity'); 6 | System.assertNotEquals(NULL, result, 'Result not NULL'); 7 | System.assertNotEquals(0, result.size(), 'Result size > 0'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaBuilderControllerTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaEvaluator.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/FormulaEvaluatorTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/RegExps.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/SearchUtils.cls: -------------------------------------------------------------------------------- 1 | global with sharing class SearchUtils { 2 | 3 | static final Map TYPE_TO_SOBJECT = new Map{ 4 | 'User' => 'User', 5 | 'Role_subordinates' => 'UserRole', 6 | 'Role' => 'UserRole', 7 | 'Group' => 'Group', 8 | 'Queue' => 'Group' 9 | }; 10 | 11 | static final Map TYPE_TO_QUERY_CRITERIA = new Map{ 12 | 'User' => 'WHERE IsActive = true AND Name LIKE (searchString)', 13 | 'Role_subordinates' => 'WHERE Name LIKE (searchString)', 14 | 'Role' => 'WHERE Name LIKE (searchString)', 15 | 'Group' => 'WHERE (Type = \'Public\' OR Type = \'Regular\') AND Name LIKE (searchString)', 16 | 'Queue' => 'WHERE Type = \'Queue\' AND Name LIKE (searchString)' 17 | }; 18 | 19 | static final List OTHER_TYPES = new List{ 20 | 'RelatedUsers', 'Owner', 'Creator' 21 | }; 22 | 23 | public static final Map TYPE_TO_ID_FIELD = new Map{ 24 | 'User' => 'UserName', 25 | 'Group' => 'DeveloperName', 26 | 'Queue' => 'DeveloperName', 27 | 'Role' => 'DeveloperName', 28 | 'Role_subordinates' => 'DeveloperName' 29 | }; 30 | 31 | 32 | @AuraEnabled(cacheable=true) 33 | public static Map> searchMemberByType(List memberTypes, String searchString) { 34 | Map> resultMap = new Map>(); 35 | 36 | for (String curType : TYPE_TO_SOBJECT.keySet()) { 37 | 38 | if (!memberTypes.contains(curType)) { 39 | continue; 40 | } 41 | 42 | String queryString = 'SELECT ' + getQueriedFields(curType) + ' FROM ' + TYPE_TO_SOBJECT.get(curType) + ' ' + TYPE_TO_QUERY_CRITERIA.get(curType).replace('(searchString)', '\'%' + String.escapeSingleQuotes(searchString) + '%\''); 43 | 44 | List types = Database.query(queryString); 45 | List members = new List(); 46 | for (SObject t : types) { 47 | members.add(new Member((String) t.get('Name'), (String) t.get(getIdField(curType)))); 48 | } 49 | resultMap.put(curType, members); 50 | } 51 | 52 | return resultMap; 53 | } 54 | @AuraEnabled(cacheable=true) 55 | public static Map getSingleMembersByTypeAndId(String type, String id) { 56 | return getMembersByTypeAndId(new Map>{ 57 | type => new Set{ 58 | id 59 | } 60 | }); 61 | } 62 | public static Map getMembersByTypeAndId(Map> typeToIds) { 63 | 64 | Map results = new Map(); 65 | for (String sObjectTypeName : typeToIds.keySet()) { 66 | if (OTHER_TYPES.contains(sObjectTypeName)) { 67 | for (String curMember : typeToIds.get(sObjectTypeName)) { 68 | results.put(curMember, new Account(Name = curMember)); 69 | } 70 | } else { 71 | Set objectIds = typeToIds.get(sObjectTypeName); 72 | 73 | String idField = getIdField(sObjectTypeName); 74 | String queryString = 'SELECT ' + getQueriedFields(sObjectTypeName) + ' FROM ' + TYPE_TO_SOBJECT.get(sObjectTypeName) + ' WHERE ' + idField + ' IN: objectIds'; 75 | List members = Database.query(queryString); 76 | for (SObject curMember : members) { 77 | results.put((String) curMember.get(idField), curMember); 78 | } 79 | } 80 | } 81 | return results; 82 | } 83 | 84 | private static String getIdField(String objectType) { 85 | return TYPE_TO_ID_FIELD.containsKey(objectType) ? TYPE_TO_ID_FIELD.get(objectType) : 'Id'; 86 | } 87 | 88 | private static String getQueriedFields(String objectType) { 89 | String requiredFields = 'Id, Name'; 90 | if (TYPE_TO_ID_FIELD.containsKey(objectType)) { 91 | requiredFields += ', ' + TYPE_TO_ID_FIELD.get(objectType); 92 | } 93 | return requiredFields; 94 | } 95 | 96 | @AuraEnabled(cacheable=true) 97 | public static Map> describeSObjects(List types) { 98 | 99 | Map> objectToFieldDescribe = new Map>(); 100 | if (types == null || types.isEmpty()) { 101 | return objectToFieldDescribe; 102 | } 103 | 104 | Schema.DescribeSobjectResult[] results = Schema.describeSObjects(types); 105 | 106 | for (Schema.DescribeSobjectResult res : results) { 107 | String objName = res.getName(); 108 | objectToFieldDescribe.put(objName, new List()); 109 | Map fieldMap = res.fields.getMap(); 110 | for (String fieldApiName : fieldMap.keySet()) { 111 | Schema.DescribeFieldResult fieldDescribe = fieldMap.get(fieldApiName).getDescribe(); 112 | objectToFieldDescribe.get(res.getName()).add(new Member(fieldDescribe.getLabel(), fieldDescribe.getName(), objName, fieldDescribe.getType().name())); 113 | } 114 | } 115 | 116 | return objectToFieldDescribe; 117 | } 118 | 119 | global class Member { 120 | @AuraEnabled global String label; 121 | @AuraEnabled global String value; 122 | @AuraEnabled global String dataType; 123 | @AuraEnabled global String type; 124 | public Member(String label, String value) { 125 | this.label = label; 126 | this.value = value; 127 | } 128 | public Member(String label, String value, String type, String dataType) { 129 | this.label = label; 130 | this.value = value; 131 | this.dataType = dataType; 132 | this.type = type; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /mdapi/classes/SearchUtils.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/classes/SearchUtilsTest.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class SearchUtilsTest { 3 | 4 | static final List TYPES_TO_SEARCH = new List{ 5 | 'User', 'Queue' 6 | }; 7 | static final String TEST_RECORD_NAME = 'testrecordname@test.te'; 8 | 9 | @testSetup 10 | static void setup() { 11 | List adminProfile = [SELECT Id FROM Profile WHERE Name = 'System Administrator']; 12 | User testUser = new User(Alias = 'test1', Email = 'testuser1@testorg.com', EmailEncodingKey = 'UTF-8', LastName = TEST_RECORD_NAME, LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US', ProfileId = adminProfile[0].Id, TimeZoneSidKey = 'America/Los_Angeles', UserName = TEST_RECORD_NAME); 13 | insert testUser; 14 | Group userGroup = new Group(Name = TEST_RECORD_NAME, type = 'Queue'); 15 | insert userGroup; 16 | QueuesObject queue = new QueueSObject(QueueID = userGroup.id, SobjectType = 'lead'); 17 | insert queue; 18 | } 19 | 20 | @IsTest 21 | static void testSearchMemberByType() { 22 | User testUser = [SELECT Id, Name, UserName FROM User WHERE Username = :TEST_RECORD_NAME]; 23 | Group testQueue = [SELECT Id, Name,DeveloperName FROM Group WHERE Type = 'Queue' AND Name = :TEST_RECORD_NAME]; 24 | 25 | Map typeToObject = new Map{ 26 | 'User' => testUser, 'Queue' => testQueue 27 | }; 28 | Map> searchResult = SearchUtils.searchMemberByType(TYPES_TO_SEARCH, TEST_RECORD_NAME); 29 | 30 | //We have added one record to each type, so expecting as many records as types 31 | System.assertEquals(TYPES_TO_SEARCH.size(), searchResult.size()); 32 | 33 | //For each type checking if search worked fine 34 | for (String typeName : TYPES_TO_SEARCH) { 35 | System.assertEquals(typeToObject.get(typeName).get(SearchUtils.TYPE_TO_ID_FIELD.get(typeName)), searchResult.get(typeName)[0].value); 36 | System.assertEquals(typeToObject.get(typeName).get('Name'), searchResult.get(typeName)[0].label); 37 | } 38 | 39 | //Checking that get name by id works fine 40 | Map> typeToIds = new Map>{ 41 | 'User' => (new Set{ 42 | testUser.UserName 43 | }), 'Queue' => (new Set{ 44 | testQueue.DeveloperName 45 | }) 46 | }; 47 | Map members = SearchUtils.getMembersByTypeAndId(typeToIds); 48 | 49 | System.assertEquals(TEST_RECORD_NAME, members.get(testUser.UserName).get('Name')); 50 | System.assertEquals(TEST_RECORD_NAME, members.get(testQueue.DeveloperName).get('Name')); 51 | } 52 | } -------------------------------------------------------------------------------- /mdapi/classes/SearchUtilsTest.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /mdapi/flows/TestFlow_EvaluateFormula.flow: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Evaluate_Formula 5 | 6 | 476 7 | 194 8 | EvaluateFormula 9 | apex 10 | 11 | screenOutput 12 | 13 | 14 | formulaString 15 | 16 | Enter_Formula 17 | 18 | 19 | 20 | recordId 21 | 22 | record_id 23 | 24 | 25 | 26 | formulaResult 27 | stringResult 28 | 29 | 30 | 31 | Evaluate_Formula2 32 | 33 | 477 34 | 354 35 | EvaluateFormula 36 | apex 37 | 38 | screenOutput 39 | 40 | 41 | formulaString 42 | 43 | formulaString 44 | 45 | 46 | 47 | recordId 48 | 49 | record_id 50 | 51 | 52 | 53 | formulaResult 54 | stringResult 55 | 56 | 57 | TestFlow_EvaluateFormula {!$Flow.CurrentDateTime} 58 | 59 | 60 | BuilderType 61 | 62 | LightningFlowBuilder 63 | 64 | 65 | 66 | OriginBuilderType 67 | 68 | LightningFlowBuilder 69 | 70 | 71 | Flow 72 | 73 | screenBuildFormula 74 | 75 | 162 76 | 342 77 | true 78 | true 79 | true 80 | 81 | Evaluate_Formula2 82 | 83 | 84 | true 85 | true 86 | 87 | 88 | screenInput 89 | 90 | 164 91 | 51 92 | true 93 | true 94 | true 95 | 96 | Evaluate_Formula 97 | 98 | 99 | Enter_Formula 100 | String 101 | Enter Formula 102 | InputField 103 | false 104 | 105 | true 106 | true 107 | 108 | 109 | screenOutput 110 | 111 | 775 112 | 208 113 | true 114 | true 115 | true 116 | 117 | dispResult 118 | <p>The result is: {!formulaResult}</p> 119 | DisplayText 120 | 121 | true 122 | true 123 | 124 | screenBuildFormula 125 | Draft 126 | 127 | formulaResult 128 | String 129 | false 130 | false 131 | false 132 | 133 | 134 | formulaString 135 | String 136 | false 137 | false 138 | false 139 | 140 | 141 | record_id 142 | String 143 | false 144 | true 145 | true 146 | 147 | 148 | -------------------------------------------------------------------------------- /mdapi/lwc/buttonUtils/buttonUtils.js: -------------------------------------------------------------------------------- 1 | export { 2 | generateCapabilityColumns, 3 | splitValues 4 | }; 5 | 6 | const generateCapabilityColumns = (labels) => { 7 | let labelsArray = labels.replace(/ /g, '').split(','); 8 | return labelsArray.map(curLabel => { 9 | return getColumnDescriptor(curLabel); 10 | }); 11 | }; 12 | 13 | const getColumnDescriptor = (curButtonLabel) => { 14 | return { 15 | type: 'button', 16 | label: curButtonLabel, 17 | typeAttributes: { 18 | label: curButtonLabel, 19 | name: curButtonLabel, //this is used to determine an apex method to call 20 | variant: 'neutral', 21 | disabled: {fieldName: curButtonLabel.replace(/ /g, '') + 'buttonDisabled'} 22 | }, 23 | initialWidth: 120 //TODO: Calculate based on content 24 | } 25 | }; 26 | 27 | const splitValues = (originalString) => { 28 | if (originalString) { 29 | return originalString.replace(/ /g, '').split(','); 30 | } else { 31 | return []; 32 | } 33 | }; 34 | 35 | 36 | -------------------------------------------------------------------------------- /mdapi/lwc/buttonUtils/buttonUtils.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | -------------------------------------------------------------------------------- /mdapi/lwc/expressionBuilder/expressionBuilder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/expressionBuilder/expressionBuilder.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | expressionBuilder 5 | true 6 | expressionBuilder 7 | 8 | lightning__RecordPage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mdapi/lwc/expressionLine/expressionLine.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/expressionLine/expressionLine.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track} from 'lwc'; 2 | 3 | export default class expressionLine extends LightningElement { 4 | 5 | @api objectType; 6 | @api operator; 7 | @api value; 8 | @api expressionId; 9 | @api expressionIndex; 10 | @api availableMergeFields = []; 11 | @track _fields = []; 12 | 13 | @track currentField; 14 | @track _fieldName; 15 | @track allOperators = [ 16 | {value: 'equals',label: 'Equals',types: 'ID,BOOLEAN,REFERENCE,STRING,EMAIL,PICKLIST,TEXTAREA,DATETIME,PHONE,DOUBLE,ADDRESS,INTEGER,URL'}, 17 | {value: 'not_equal_to',label: 'Not Equal To', types: 'ID,BOOLEAN,REFERENCE,STRING,EMAIL,PICKLIST,TEXTAREA,DATETIME,PHONE,DOUBLE,ADDRESS,INTEGER,URL'}, 18 | {value: 'greater_then', label: 'Greater than', types: 'DOUBLE,INTEGER,DATETIME'}, 19 | {value: 'greater_or_equal', label: 'Greater Or Equal', types: 'DOUBLE,INTEGER,DATETIME'}, 20 | {value: 'less_then', label: 'Less Than', types: 'DOUBLE,INTEGER,DATETIME'}, 21 | {value: 'less_or_equal', label: 'Less Or Equal', types: 'DOUBLE,INTEGER,DATETIME'}, 22 | {value: 'contains', label: 'Contains', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'}, 23 | {value: 'starts_with', label: 'Starts with', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'}, 24 | {value: 'end_with', label: 'End with', types: 'ID,STRING,EMAIL,PICKLIST,TEXTAREA,PHONE,ADDRESS,URL'} 25 | ]; 26 | 27 | @track filterValue = ''; 28 | 29 | initialized = false; 30 | 31 | renderedCallback() { 32 | if (this.initialized) { 33 | return; 34 | } 35 | this.initialized = true; 36 | let listId = this.template.querySelector('datalist').id; 37 | this.template.querySelector("input").setAttribute("list", listId); 38 | } 39 | 40 | @api 41 | get fields() { 42 | return this._fields; 43 | } 44 | 45 | set fields(value) { 46 | this._fields = value; 47 | this.setCurrentField(); 48 | } 49 | 50 | 51 | @api 52 | get fieldName() { 53 | return this._fieldName; 54 | 55 | } 56 | 57 | set fieldName(value) { 58 | this._fieldName = value; 59 | this.setCurrentField(); 60 | } 61 | 62 | get availableOperators() { 63 | if (this.currentField) { 64 | return this.allOperators.filter(curOperator => curOperator.types.includes(this.currentField.dataType.toUpperCase())); 65 | } else { 66 | return []; 67 | } 68 | } 69 | 70 | selectField(event) { 71 | let eventValue = event.detail.value; 72 | if (eventValue) { 73 | this._fieldName = eventValue; 74 | this.setCurrentField(); 75 | this.dispatchChangeEvent({ 76 | id: this.expressionId, 77 | fieldName: eventValue 78 | }); 79 | } 80 | } 81 | 82 | setCurrentField() { 83 | if (this._fields && this._fields.length && this._fieldName) { 84 | if (!this.currentField || this.currentField.value !== this._fieldName) { 85 | this.currentField = this._fields.find(curField => curField.value === this._fieldName); 86 | } 87 | } 88 | } 89 | 90 | get disabledFilter() { 91 | return !this._fieldName; 92 | } 93 | 94 | handleOperatorChange(event) { 95 | this.dispatchChangeEvent({ 96 | id: this.expressionId, 97 | operator: event.detail.value 98 | }); 99 | } 100 | 101 | handleValueChange(event) { 102 | this.dispatchChangeEvent({ 103 | id: this.expressionId, 104 | parameter: event.target.value 105 | }); 106 | } 107 | 108 | dispatchChangeEvent(customParams) { 109 | const memberRefreshedEvt = new CustomEvent('fieldselected', { 110 | bubbles: true, 111 | detail: {...this.currentField, ...customParams} 112 | }); 113 | this.dispatchEvent(memberRefreshedEvt); 114 | } 115 | 116 | handleExpressionRemove() { 117 | const expressionRemovedEvent = new CustomEvent('expressionremoved', { 118 | bubbles: true, detail: this.expressionId 119 | }); 120 | this.dispatchEvent(expressionRemovedEvent); 121 | } 122 | 123 | get position() { 124 | return this.expressionIndex + 1; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /mdapi/lwc/expressionLine/expressionLine.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | expressionLine 5 | true 6 | expressionLine 7 | 8 | -------------------------------------------------------------------------------- /mdapi/lwc/formulaBuilder/formulaBuilder.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: var(--lwc-colorBackgroundAlt); 3 | padding: var(--lwc-spacingMedium); 4 | border-radius: var(--lwc-borderRadiusMedium); 5 | position: relative; 6 | } 7 | .flexContainer { 8 | display: flex; 9 | padding-bottom: var(--lwc-spacingMedium); 10 | align-items: center; 11 | } 12 | .flexContainer p { 13 | padding-right: var(--lwc-spacingXSmall); 14 | } 15 | lightning-combobox { 16 | padding-right: var(--lwc-spacingXSmall); 17 | } 18 | lightning-button { 19 | margin-left: 0; 20 | } 21 | .resultContainer { 22 | display: flex; 23 | padding-top: var(--lwc-spacingXSmall); 24 | padding-bottom: var(--lwc-spacingXSmall); 25 | } -------------------------------------------------------------------------------- /mdapi/lwc/formulaBuilder/formulaBuilder.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/formulaBuilder/formulaBuilder.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, track, api, wire} from 'lwc'; 2 | import {FlowAttributeChangeEvent} from 'lightning/flowSupport'; 3 | import describeSObjects from '@salesforce/apex/SearchUtils.describeSObjects'; 4 | 5 | 6 | export default class FormulaBuilder extends LightningElement { 7 | 8 | @api functions; 9 | @api operators; 10 | @api supportedSystemTypes; 11 | @api contextDataString; 12 | 13 | @track formula = ''; 14 | @track _objectName; 15 | @track contextFields = []; 16 | @track contextTypes; 17 | 18 | @track functionValue = null; 19 | 20 | @api get contextObjectType() { 21 | return this._objectName; 22 | } 23 | 24 | set contextObjectType(value) { 25 | this._objectName = value; 26 | if (value) { 27 | this.contextTypes = [...[value], ...this.supportedSystemTypes ? this.splitValues(this.supportedSystemTypes) : []]; 28 | } 29 | } 30 | 31 | @api supportedFunctions = [ 32 | 'AND', 'OR', 'NOT', 'XOR', 'IF', 'CASE', 'LEN', 'SUBSTRING', 'LEFT', 'RIGHT', 33 | 'ISBLANK', 'ISPICKVAL', 'CONVERTID', 'ABS', 'ROUND', 'CEILING', 'FLOOR', 'SQRT', 'ACOS', 34 | 'ASIN', 'ATAN', 'COS', 'SIN', 'TAN', 'COSH', 'SINH', 'TANH', 'EXP', 'LOG', 'LOG10', 'RINT', 35 | 'SIGNUM', 'INTEGER', 'POW', 'MAX', 'MIN', 'MOD', 'TEXT', 'DATETIME', 'DECIMAL', 'BOOLEAN', 36 | 'DATE', 'DAY', 'MONTH', 'YEAR', 'HOURS', 'MINUTES', 'SECONDS', 'ADDDAYS', 'ADDMONTHS', 37 | 'ADDYEARS', 'ADDHOURS', 'ADDMINUTES', 'ADDSECONDS', 'CONTAINS', 'FIND', 'LOWER', 'UPPER' 38 | , 'MID', 'SUBSTITUTE', 'TRIM', 'VALUE', 'CONCATENATE', 'TODAY=>$TODAY', 'WEEKDAY', 'BEGINS' 39 | ]; 40 | 41 | @api supportedOperators = ['+', '-', '/', '*', '==', '!=', '>', '<', '>=', '<=', '<>']; 42 | 43 | @api 44 | get formulaString() { 45 | return this.formula; 46 | } 47 | 48 | set formulaString(value) { 49 | this.formula = value; 50 | } 51 | 52 | @wire(describeSObjects, {types: '$contextTypes'}) 53 | _describeSObjects(result) { 54 | if (result.error) { 55 | console.log(result.error.body.message); 56 | // this.errors.push(error.body[0].message); 57 | } else if (result.data) { 58 | this.contextTypes.forEach(objType => { 59 | 60 | let newContextFields = result.data[objType].map(curField => { 61 | return {label: objType + ': ' + curField.label, value: '$' + objType + '.' + curField.value} 62 | }); 63 | 64 | if (this.contextFields) { 65 | this.contextFields = this.contextFields.concat(newContextFields); 66 | } else { 67 | this.contextFields = newContextFields; 68 | } 69 | 70 | }); 71 | if (this.contextDataString) { 72 | let contextDataObj = JSON.parse(this.contextDataString); 73 | contextDataObj.forEach(curEl => { 74 | this.contextFields.push({label: 'Context Data: ' + curEl.name, value: curEl.value}); 75 | }) 76 | } 77 | } 78 | } 79 | 80 | formulaChangedEvent() { 81 | const memberRefreshedEvt = new CustomEvent('formulachanged', { 82 | bubbles: true, detail: { 83 | value: this.formula 84 | } 85 | }); 86 | this.dispatchEvent(memberRefreshedEvt); 87 | } 88 | 89 | dispatchFormulaChangedEvents() { 90 | this.formulaChangedFlowEvent(); 91 | this.formulaChangedEvent(); 92 | } 93 | 94 | setFunctions() { 95 | let functions = []; 96 | this.supportedFunctions.sort((a, b) => (a > b) ? 1 : ((b > a) ? -1 : 0)).forEach(func => { 97 | let funcParts = func.split('=>'); 98 | functions.push({label: funcParts[0], value: funcParts.length == 1 ? funcParts[0] + '()' : funcParts[1]}); 99 | }); 100 | this.functions = functions; 101 | } 102 | 103 | connectedCallback() { 104 | 105 | this.setFunctions(); 106 | let operators = []; 107 | 108 | this.supportedOperators.forEach(operator => { 109 | operators.push({label: operator, value: operator}); 110 | }); 111 | 112 | this.operators = operators; 113 | 114 | } 115 | 116 | formulaChangedFlowEvent(event) { 117 | const valueChangeEvent = new FlowAttributeChangeEvent('value', this.formula); 118 | this.dispatchEvent(valueChangeEvent); 119 | } 120 | 121 | insertInCurrentPosition(value, setCursor) { 122 | let formulaTextArea = this.template.querySelector('.slds-textarea'); 123 | if (formulaTextArea) { 124 | if (value !== '') { 125 | let formulaStart = this.formula.substring(0, formulaTextArea.selectionStart) + ' ' + value + ' '; 126 | let newFormulaString = formulaStart + this.formula.substring(formulaTextArea.selectionEnd); 127 | this.formula = newFormulaString; 128 | formulaTextArea.value = newFormulaString; 129 | if (setCursor) { 130 | formulaTextArea.focus(); 131 | formulaTextArea.selectionEnd = formulaStart.length - 2; 132 | } 133 | this.dispatchFormulaChangedEvents(); 134 | } 135 | } 136 | 137 | } 138 | 139 | selectOperator(event) { 140 | this.insertInCurrentPosition(event.detail.value); 141 | this.clearSelections(); 142 | } 143 | 144 | selectFunction(event) { 145 | this.insertInCurrentPosition(event.detail.value, true); 146 | this.clearSelections(); 147 | } 148 | 149 | clearSelections() { 150 | this.template.querySelectorAll('lightning-combobox').forEach(curComboBox => { 151 | curComboBox.value = null; 152 | }); 153 | } 154 | 155 | changeFormula(event) { 156 | this.formula = event.target.value; 157 | this.dispatchFormulaChangedEvents(); 158 | } 159 | 160 | selectField(event) { 161 | this.insertInCurrentPosition(event.detail.value, false); 162 | this.clearSelections(); 163 | } 164 | 165 | splitValues(originalString) { 166 | return originalString ? originalString.replace(/ /g, '').split(',') : []; 167 | } 168 | } -------------------------------------------------------------------------------- /mdapi/lwc/formulaBuilder/formulaBuilder.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | true 5 | 6 | lightning__AppPage 7 | lightning__RecordPage 8 | lightning__HomePage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /mdapi/lwc/lwcLogger/lwcLogger.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | //WARNING inverted logOn because I can't figure out how to efficiently turn on logging 4 | 5 | /* eslint-disable no-console */ 6 | const logger = ( 7 | logOn = false, 8 | source = 'unspecified source', 9 | message, 10 | data, 11 | ) => { 12 | if (!logOn) { 13 | try { 14 | if (data) { 15 | console.log( 16 | `${source}: ${message}`, 17 | JSON.parse(JSON.stringify(data)), 18 | ); 19 | } else { 20 | console.log(`${source}: ${message}`); 21 | } 22 | } catch (e) { 23 | if (data) { 24 | console.log(`${source}: ${message}`, data); 25 | } else { 26 | console.log(`${source}: ${message}`); 27 | } 28 | } 29 | } 30 | }; 31 | 32 | const logError = ( 33 | logOn = false, 34 | source = 'unspecified source', 35 | message, 36 | data, 37 | ) => { 38 | if (!logOn) { 39 | try { 40 | if (data) { 41 | console.log( 42 | `${source}: ${message}`, 43 | JSON.parse(JSON.stringify(data)), 44 | ); 45 | } else { 46 | console.log(`${source}: ${message}`); 47 | } 48 | } catch (e) { 49 | if (data) { 50 | console.log(`${source}: ${message}`, data); 51 | } else { 52 | console.log(`${source}: ${message}`); 53 | } 54 | } 55 | } 56 | }; 57 | 58 | export { logger, logError }; 59 | -------------------------------------------------------------------------------- /mdapi/lwc/lwcLogger/lwcLogger.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | false 5 | 6 | -------------------------------------------------------------------------------- /mdapi/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.css: -------------------------------------------------------------------------------- 1 | .spinner-holder{ 2 | width: 80px; 3 | height: 80px; 4 | } 5 | .field-picker{ 6 | position: relative; 7 | } -------------------------------------------------------------------------------- /mdapi/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track, wire} from 'lwc'; 2 | import {FlowAttributeChangeEvent} from 'lightning/flowSupport'; 3 | import {getObjectInfo} from 'lightning/uiObjectInfoApi'; 4 | import getObjects from '@salesforce/apex/FieldPickerController.getObjects'; 5 | 6 | import NonePicklistValueLabel from '@salesforce/label/c.NonePicklistValueLabel'; 7 | import FieldIsNotSupportedMessage from '@salesforce/label/c.FieldIsNotSupportedMessage'; 8 | 9 | export default class pickObjectAndFieldFSC extends LightningElement { 10 | @api masterLabel; 11 | @api objectLabel = 'Object'; 12 | @api fieldLabel = 'Field'; 13 | @api objectType; 14 | @api field; 15 | @api availableFields; 16 | 17 | @api disableObjectPicklist = false; 18 | @api hideObjectPicklist = false; 19 | @api hideFieldPicklist = false; 20 | @api displayFieldType = false; 21 | @api prependObjectNameToFieldLabel = false; 22 | 23 | @track _objectType; 24 | @track _field; 25 | @track _availableObjectTypes; 26 | @track availableObjectTypesList = []; 27 | @track objectTypes; 28 | @track fields; 29 | @track errors = []; 30 | @track isLoadFinished = false; 31 | 32 | labels = { 33 | none: NonePicklistValueLabel, 34 | fieldNotSupported: FieldIsNotSupportedMessage 35 | }; 36 | 37 | connectedCallback() { 38 | if (this.objectType) { 39 | this._objectType = this.objectType; 40 | if (this.disableObjectPicklist || this.hideObjectPicklist) { 41 | this.availableObjectTypesList = this._availableObjectTypes ? this.splitValues(this._supportedObjectTypes.toLowerCase()) : [this._objectType]; 42 | } 43 | } 44 | 45 | if (this.objectType && this.field) 46 | this._field = this.field; 47 | } 48 | 49 | @wire(getObjects, {availableObjectTypes: '$availableObjectTypesList'}) 50 | _getObjects({error, data}) { 51 | if (error) { 52 | this.errors.push(error.body.message); 53 | } else if (data) { 54 | this.isLoadFinished = true; 55 | this.objectTypes = data; 56 | } 57 | } 58 | 59 | @wire(getObjectInfo, {objectApiName: '$_objectType'}) 60 | _getObjectInfo({error, data}) { 61 | if (error) { 62 | this.errors.push(error.body[0].message); 63 | } else if (data) { 64 | let objectLabel = data.label; 65 | let fields = data.fields; 66 | let fieldResults = []; 67 | for (let field in this.fields = fields) { 68 | if (Object.prototype.hasOwnProperty.call(fields, field)) { 69 | if (this.isTypeSupported(fields[field])) { 70 | fieldResults.push({ 71 | label: this.prependObjectNameToFieldLabel ? objectLabel + ': ' + fields[field].label : fields[field].label, 72 | value: fields[field].apiName, 73 | dataType: fields[field].dataType, 74 | required: fields[field].required, 75 | updateable: fields[field].updateable, 76 | referenceTo: (fields[field].referenceToInfos.length > 0 ? fields[field].referenceToInfos.map(curRef => { 77 | return curRef.apiName 78 | }) : []) 79 | }); 80 | } 81 | } 82 | if (this._field && !Object.prototype.hasOwnProperty.call(fields, this._field)) { 83 | this.errors.push(this.labels.fieldNotSupported + this._field); 84 | this._field = null; 85 | } 86 | } 87 | if (fieldResults) { 88 | fieldResults.sort((a, b) => (a.label > b.label) ? 1 : ((b.label > a.label) ? -1 : 0)) 89 | } 90 | 91 | this.fields = fieldResults; 92 | 93 | if (this.fields) { 94 | this.dispatchDataChangedEvent({...this.fields.find(curField => curField.value == this._field), ...{isInit: true}}); 95 | } 96 | } 97 | } 98 | 99 | get isFieldTypeVisible() { 100 | return (this.fieldType && this.displayFieldType); 101 | } 102 | 103 | isTypeSupported(field) { 104 | let result = false; 105 | if (!this.availableFields) { 106 | result = true; 107 | } 108 | if (!result && field.referenceToInfos.length > 0) { 109 | field.referenceToInfos.forEach(curRef => { 110 | if (this.availableFields.toLowerCase().includes(curRef.apiName.toLowerCase())) { 111 | result = true; 112 | } 113 | }); 114 | } 115 | return result; 116 | } 117 | 118 | @api get availableObjectTypes() { 119 | return this._availableObjectTypes; 120 | } 121 | 122 | set availableObjectTypes(value) { 123 | this._availableObjectTypes = value; 124 | this.availableObjectTypesList = this._availableObjectTypes ? this.splitValues(this._supportedObjectTypes.toLowerCase()) : [this._objectType]; 125 | } 126 | 127 | get isError() { 128 | return this.errors.length > 0; 129 | } 130 | 131 | get errorMessage() { 132 | return this.errors.join('; '); 133 | } 134 | 135 | get isFieldDisabled() { 136 | return this._objectType == null || this.isError; 137 | } 138 | 139 | get fieldType() { 140 | if (this._field) { 141 | return this.fields.find(e => e.value == this._field).dataType; 142 | } else { 143 | return null; 144 | } 145 | } 146 | 147 | handleObjectChange(event) { 148 | this._objectType = event.detail.value; 149 | this._field = null; 150 | this.dispatchDataChangedEvent({}); 151 | const attributeChangeEvent = new FlowAttributeChangeEvent('objectType', this._objectType); 152 | this.dispatchEvent(attributeChangeEvent); 153 | this.errors = []; 154 | } 155 | 156 | handleFieldChange(event) { 157 | this._field = event.detail.value; 158 | this.dispatchDataChangedEvent(this.fields.find(curField => curField.value == this._field)); 159 | const attributeChangeEvent = new FlowAttributeChangeEvent('field', this._field); 160 | this.dispatchEvent(attributeChangeEvent); 161 | } 162 | 163 | dispatchDataChangedEvent(detail) { 164 | const memberRefreshedEvt = new CustomEvent('fieldselected', { 165 | bubbles: true, 166 | detail: { 167 | ...detail, ...{ 168 | objectType: this._objectType, 169 | fieldName: this._field 170 | } 171 | } 172 | }); 173 | this.dispatchEvent(memberRefreshedEvt); 174 | 175 | } 176 | 177 | splitValues(originalString) { 178 | return originalString ? originalString.replace(/ /g, '').split(',') : []; 179 | }; 180 | } -------------------------------------------------------------------------------- /mdapi/lwc/pickObjectAndFieldFSC/pickObjectAndFieldFSC.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 47.0 4 | Pick Object And Field FSC 5 | true 6 | Pick Object And Field FSC 7 | 8 | lightning__FlowScreen 9 | lightning__RecordPage 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /mdapi/lwc/setOwner/setOwner.css: -------------------------------------------------------------------------------- 1 | .button-align-bottom { 2 | align-self: flex-end; 3 | } -------------------------------------------------------------------------------- /mdapi/lwc/setOwner/setOwner.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/setOwner/setOwner.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track, wire} from 'lwc'; 2 | 3 | import Search from '@salesforce/label/c.Search'; 4 | import For from '@salesforce/label/c.For'; 5 | import TooManyResultsMessage from '@salesforce/label/c.TooManyResultsMessage'; 6 | import Queues from '@salesforce/label/c.Queues'; 7 | import RelatedUsers from '@salesforce/label/c.RelatedUsers'; 8 | import PublicGroups from '@salesforce/label/c.PublicGroups'; 9 | import Roles from '@salesforce/label/c.Roles'; 10 | import Users from '@salesforce/label/c.Users'; 11 | 12 | import USER_NAME_FIELD from '@salesforce/schema/User.Name'; 13 | import GROUP_NAME_FIELD from '@salesforce/schema/Group.Name'; 14 | 15 | import searchMemberByType from '@salesforce/apex/SearchUtils.searchMemberByType'; 16 | 17 | import getSingleMembersByTypeAndId from '@salesforce/apex/SearchUtils.getSingleMembersByTypeAndId'; 18 | import {ShowToastEvent} from 'lightning/platformShowToastEvent'; 19 | import {logger, logError} from 'c/lwcLogger'; 20 | 21 | import { 22 | generateCapabilityColumns, 23 | splitValues 24 | } from 'c/buttonUtils'; 25 | 26 | 27 | const typeMapping = { 28 | Group: PublicGroups, 29 | Role: Roles, 30 | User: Users, 31 | Queue: Queues, 32 | RelatedUsers: RelatedUsers 33 | }; 34 | 35 | export default class addNewMembers extends LightningElement { 36 | @api notifyAssignee = false; 37 | @api memberId; 38 | @api availableObjectTypes; 39 | @api supportedAddCapabilities; 40 | @api notifyAssigneeLabel; 41 | 42 | @track memberData; 43 | @track showMemberSelect = false; 44 | @track label = { 45 | Search, 46 | TooManyResultsMessage, 47 | For 48 | }; 49 | @track searchString = ''; 50 | @track selectedType; 51 | @track searchResults = []; 52 | @track searchDisabled = false; 53 | source = 'ownerSetter'; 54 | 55 | @wire(getSingleMembersByTypeAndId, {type: '$selectedType', id: '$memberId'}) 56 | _getSingleMembersByTypeAndId({error, data}) { 57 | if (error) { 58 | console.log(error.body.message); 59 | } else if (data) { 60 | this.memberData = data[this.memberId]; 61 | } 62 | } 63 | 64 | connectedCallback() { 65 | if (this.availableObjectTypes && this.availableObjectTypes.length > 0) { 66 | this.selectedType = splitValues(this.availableObjectTypes)[0]; 67 | } 68 | } 69 | 70 | get objectTypes() { 71 | return splitValues(this.availableObjectTypes).map(curTypeName => { 72 | return this.getTypeDescriptor(curTypeName); 73 | }); 74 | } 75 | 76 | getTypeDescriptor(typeName) { 77 | return {value: typeName, label: typeMapping[typeName]}; 78 | } 79 | 80 | get tooManyResults() { 81 | return this.searchResults.length > 199; 82 | } 83 | 84 | get columns() { 85 | return [{ 86 | label: 'Name', 87 | fieldName: 'label' 88 | }].concat(generateCapabilityColumns(this.supportedAddCapabilities)); 89 | } 90 | 91 | get showNotifyAssignee() { 92 | return this.selectedType === 'User'; 93 | } 94 | 95 | get selectedMemberName() { 96 | if (this.memberData) { 97 | return this.memberData.Name; 98 | } else { 99 | return null; 100 | } 101 | } 102 | 103 | handleTypeChange(event) { 104 | this.selectedType = event.detail.value; 105 | this.searchResults = []; 106 | } 107 | 108 | async actuallySearch() { 109 | this.searchResults = []; 110 | this.searchDisabled = true; 111 | 112 | const results = 113 | await searchMemberByType({ 114 | searchString: this.searchString, 115 | memberTypes: [this.selectedType] 116 | }); 117 | this.searchResults = results[this.selectedType]; 118 | 119 | this.searchDisabled = false; 120 | } 121 | 122 | searchEventHandler(event) { 123 | if (event.detail.value) { 124 | const searchString = event.detail.value 125 | .trim() 126 | .replace(/\*/g) 127 | .toLowerCase(); 128 | 129 | if (searchString.length <= 1) { 130 | return; 131 | } 132 | 133 | this.searchString = searchString; 134 | } 135 | } 136 | 137 | listenForEnter(event) { 138 | if (event.code === 'Enter') { 139 | this.actuallySearch(); 140 | } 141 | } 142 | 143 | handleRowAction(event) { 144 | const memberRefreshedEvt = new CustomEvent('membersrefreshed', { 145 | bubbles: true, detail: { 146 | memberId: event.detail.row.value, 147 | notifyAssignee: (this.selectedType === 'User' ? this.notifyAssignee : false), 148 | } 149 | }); 150 | this.dispatchEvent(memberRefreshedEvt); 151 | } 152 | 153 | toastTheError(e, errorSource) { 154 | logError(this.log, this.source, errorSource, e); 155 | this.dispatchEvent( 156 | new ShowToastEvent({ 157 | message: e.body.message, 158 | variant: 'error' 159 | }) 160 | ); 161 | } 162 | 163 | toggleMemberSelect() { 164 | this.showMemberSelect = !this.showMemberSelect; 165 | } 166 | 167 | handleNotifyAssigneeChange(event) { 168 | this.notifyAssignee = event.target.checked === true; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /mdapi/lwc/setOwner/setOwner.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | true 5 | 6 | lightning__RecordPage 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mdapi/lwc/setPickList/setPickList.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mdapi/lwc/setPickList/setPickList.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, track, wire} from 'lwc'; 2 | import getPicklistValues from '@salesforce/apex/FieldPickerController.getPicklistValues'; 3 | import NonePicklistValueLabel from '@salesforce/label/c.NonePicklistValueLabel'; 4 | 5 | export default class setPickList extends LightningElement { 6 | @api masterLabel = 'Picklist Options'; 7 | @api valueAboveRadioLabel = 'The value above the current one'; 8 | @api valueBelowRadioLabel = 'The value below the current one'; 9 | @api specificValueRadioLabel = 'A Specific Value'; 10 | @api picklistObjectName; 11 | @api picklistFieldName; 12 | @api selectionType; 13 | @api value; 14 | 15 | @track _selectionType; 16 | @track _value; 17 | @track availableValues; 18 | @track errors = []; 19 | 20 | labels = { 21 | none: NonePicklistValueLabel, 22 | previous: '__PicklistPrevious', 23 | next: '__PicklistNext', 24 | nullValue: '__null' 25 | }; 26 | 27 | connectedCallback() { 28 | // this._selectionType = this.selectionType; 29 | this._value = (this.value === null ? this.labels.nullValue : this.value); 30 | if (this._value === this.labels.next || this._value === this.labels.previous) { 31 | this._selectionType = this._value; 32 | } else { 33 | this._selectionType = this.labels.nullValue; 34 | } 35 | } 36 | 37 | @wire(getPicklistValues, {objectApiName: '$picklistObjectName', fieldName: '$picklistFieldName'}) 38 | _getPicklistValues({error, data}) { 39 | if (error) { 40 | this.errors.push(error.body.message); 41 | } else if (data) { 42 | this.availableValues = JSON.parse(JSON.stringify(data)); 43 | this.availableValues.unshift({value: this.labels.nullValue, label: this.labels.none}); 44 | } 45 | } 46 | 47 | get picklistOptions() { 48 | return [ 49 | {label: this.valueAboveRadioLabel, value: this.labels.previous}, 50 | {label: this.valueBelowRadioLabel, value: this.labels.next}, 51 | {label: this.specificValueRadioLabel, value: this.labels.nullValue}]; 52 | } 53 | 54 | get isSpecificValue() { 55 | return this._selectionType === this.labels.nullValue; 56 | } 57 | 58 | handleOptionChange(event) { 59 | this._selectionType = event.detail.value; 60 | this.handlePicklistValueChange(event); 61 | } 62 | 63 | handlePicklistValueChange(event) { 64 | const memberRefreshedEvt = new CustomEvent('picklistselected', { 65 | bubbles: true, detail: { 66 | value: (event.detail.value === this.labels.nullValue ? null : event.detail.value), 67 | selectionType: this._selectionType 68 | } 69 | }); 70 | this.dispatchEvent(memberRefreshedEvt); 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /mdapi/lwc/setPickList/setPickList.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | setPickList 5 | true 6 | setPickList 7 | 8 | -------------------------------------------------------------------------------- /mdapi/lwc/updateFieldCPE/updateFieldCPE.js: -------------------------------------------------------------------------------- 1 | import {LightningElement, api, wire, track} from 'lwc'; 2 | 3 | export default class updateFieldConfigurator extends LightningElement { 4 | 5 | @api supportedSystemTypes; 6 | 7 | @api values; 8 | @api property; 9 | @api flowContext; 10 | @api validate(){} 11 | 12 | @track _value; 13 | @track _objectType; 14 | @track _fieldName; 15 | @track selectedField; 16 | @track textOption; 17 | @track formulaEditorVisible = false; 18 | @track formulaEditorMessage = 'Show Formula Editor'; 19 | 20 | labels = { 21 | fieldTypeNotSupported: 'Selected field type is not supported', 22 | fieldValueLabel: 'Set Field Value', 23 | fieldNotUpdatable: 'Select field can not be updated' 24 | }; 25 | 26 | customReferenceTypes = ['User']; 27 | 28 | @api get objectType() { 29 | return this._objectType; 30 | } 31 | 32 | set objectType(value) { 33 | this._objectType = value; 34 | } 35 | 36 | @api get fieldName() { 37 | return this._fieldName; 38 | } 39 | 40 | set fieldName(value) { 41 | this._fieldName = value; 42 | } 43 | 44 | @api get value() { 45 | return this._value; 46 | } 47 | 48 | set value(value) { 49 | this._value = value; 50 | } 51 | 52 | get textOptions() { 53 | let resultTextOptions = []; 54 | if (this.fieldProperties && !this.fieldProperties.isRequired) { 55 | resultTextOptions.push({label: 'A blank value (null)', value: 'null'}); 56 | } 57 | resultTextOptions.push({label: 'Use a formula to set the new value', value: 'formula_builder'}); 58 | return resultTextOptions; 59 | } 60 | 61 | get checkboxOptions() { 62 | return [ 63 | {label: 'True', value: 'true'}, 64 | {label: 'False', value: 'false'}]; 65 | } 66 | 67 | handleFieldChange(event) { 68 | this.selectedField = JSON.parse(JSON.stringify(event.detail)); 69 | if (this._objectType !== this.selectedField.objectType) { 70 | this._objectType = this.selectedField.objectType; 71 | } 72 | if (this._fieldName !== this.selectedField.fieldName) { 73 | this._fieldName = this.selectedField.fieldName; 74 | } 75 | if (!this.selectedField.isInit) { 76 | this._value = null; 77 | } 78 | } 79 | 80 | handleValueChange(event) { 81 | this._value = event.detail.value; 82 | } 83 | 84 | handleOwnerChange(event) { 85 | this._value = event.detail.memberId; 86 | // event.detail.notifyAssignee; 87 | } 88 | 89 | handleTextOptionValueChange(event) { 90 | this.textOption = event.detail.value; 91 | } 92 | 93 | handleSave(event) { 94 | 95 | } 96 | 97 | toggleFormulaEditor() { 98 | this.formulaEditorVisible = !this.formulaEditorVisible; 99 | if (this.formulaEditorVisible) { 100 | this.formulaEditorMessage = 'Hide Formula Editor'; 101 | } else { 102 | this.formulaEditorMessage = 'Show Formula Editor' 103 | } 104 | } 105 | 106 | get showFormulaBuilderOption() { 107 | return this.textOption === 'formula_builder'; 108 | } 109 | 110 | get fieldProperties() { 111 | if (this.selectedField && this.selectedField.fieldName) { 112 | return { 113 | ...this.selectedField, ...{ 114 | isTextField: this.selectedField.dataType === 'String' || (this.selectedField.dataType === 'Reference' && !this.customReferenceTypes.some(refType => this.selectedField.referenceTo.includes(refType))), 115 | isUserReferenceField: this.selectedField.referenceTo.includes('User'), 116 | isBoolean: this.selectedField.dataType === 'Boolean', 117 | isPicklist: this.selectedField.dataType === 'Picklist', 118 | isDateTime: this.selectedField.dataType === 'DateTime', 119 | isDate: this.selectedField.dataType === 'Date', 120 | isCurrency: this.selectedField.dataType === 'Currency', 121 | isAddress: this.selectedField.dataType === 'Address', 122 | isDouble: this.selectedField.dataType === 'Double' || this.selectedField.dataType === 'Int', 123 | isTextArea: this.selectedField.dataType === 'TextArea', 124 | isPhone: this.selectedField.dataType === 'Phone', 125 | isUrl: this.selectedField.dataType === 'Url', 126 | isDisabled: this.selectedField.updateable !== true, 127 | isRequired: this.selectedField.required === true 128 | } 129 | } 130 | } 131 | return null; 132 | } 133 | } -------------------------------------------------------------------------------- /mdapi/lwc/updateFieldCPE/updateFieldCPE.js-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46.0 4 | updateFieldConfigurator 5 | true 6 | updateFieldConfigurator 7 | 8 | lightning__RecordPage 9 | lightning__FlowScreen 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /mdapi/package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ApexClass 5 | AssembleInputParams 6 | EvaluateFormula 7 | ExpressionBuilder 8 | ExpressionBuilderTest 9 | FieldPickerController 10 | FieldPickerControllerTest 11 | FormulaBuilderController 12 | FormulaBuilderControllerTest 13 | FormulaEvaluator 14 | FormulaEvaluatorTest 15 | RegExps 16 | SearchUtils 17 | SearchUtilsTest 18 | 19 | 20 | Flow 21 | TestFlow_EvaluateFormula 22 | 23 | 24 | CustomLabels 25 | CustomLabels 26 | 27 | 28 | CustomLabel 29 | * 30 | 31 | 32 | LightningComponentBundle 33 | buttonUtils 34 | expressionBuilder 35 | expressionLine 36 | formulaBuilder 37 | lwcLogger 38 | pickObjectAndFieldFSC 39 | setOwner 40 | setPickList 41 | updateFieldCPE 42 | 43 | 44 | StaticResource 45 | SiteSamples 46 | 47 | 47.0 48 | -------------------------------------------------------------------------------- /mdapi/staticresources/SiteSamples.resource: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexed1/FormulasAndExpressionsLWC/5aca9301a6cc7c63ce264d614a14bc102fbba412/mdapi/staticresources/SiteSamples.resource -------------------------------------------------------------------------------- /mdapi/staticresources/SiteSamples.resource-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Public 4 | application/zip 5 | Static resource for sites sample pages 6 | 7 | -------------------------------------------------------------------------------- /sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true, 6 | "package": "formulabuilder", 7 | "versionName": "ver 0.1", 8 | "versionNumber": "0.1.0.NEXT" 9 | } 10 | ], 11 | "namespace": "", 12 | "sfdcLoginUrl": "https://login.salesforce.com", 13 | "sourceApiVersion": "47.0", 14 | "packageAliases": { 15 | "formulabuilder": "0HoB0000000Cax0KAC" 16 | } 17 | } --------------------------------------------------------------------------------