wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

By Date: March 2019

Extract field definitions from object.xml


The other department asked: can I have a csv list of fields with Name,Type,length from Salesforce. The only source they have are the object.xml files from a meta data API export

Some counting required

The object.xml file contains size information for text files only. For date and numbers that's not an issue. The sticky part are pick lists and multi select pick lists.

A little XSLT goes a long way. The magic is in the expression max((s:valueSet/s:valueSetDefinition/s:value/s:fullName/string-length())) For your enjoyment, the full style sheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
    xmlns:s="http://soap.sforce.com/2006/04/metadata"
    exclude-result-prefixes="xs xd"
    version="2.0">
   
   <xsl:output method="text" encoding="UTF-8" />
    
    <xsl:template match="/">
name,type,length
<xsl:apply-templates select="/s:CustomObject/s:fields" />
    </xsl:template>
    
    <xsl:template match="s:fields">
<xsl:value-of select="s:fullName"/>,<xsl:value-of select="s:type"/>,<xsl:value-of select="s:length"/>
        <xsl:text>&#xa;</xsl:text>
    </xsl:template>    
    
    <xsl:template match="s:fields[s:type='MultiselectPicklist']">
        <xsl:value-of select="s:fullName"/>,<xsl:value-of select="s:type"/>,<xsl:value-of select="sum((s:valueSet/s:valueSetDefinition/s:value/s:fullName/string-length()))+count(s:valueSet/s:valueSetDefinition/s:value/s:fullName)" />
        <xsl:text>&#xa;</xsl:text>
    </xsl:template>
    
    <xsl:template match="s:fields[s:type='Picklist']">
        <xsl:value-of select="s:fullName"/>,<xsl:value-of select="s:type"/>,<xsl:value-of select="max((s:valueSet/s:valueSetDefinition/s:value/s:fullName/string-length()))" />
        <xsl:text>&#xa;</xsl:text>
    </xsl:template>    

<!-- stuff without use -->
<xsl:template match="s:fields[s:formula]" />
    <xsl:template match="s:fields[not(s:type)]" />
<xsl:template match="s:fields[s:type='Picklist' and not(s:valueSet)]" />  
    
</xsl:stylesheet>

One of the little stumbling blocks: a namespace for http://soap.sforce.com/2006/04/metadata is required.

As usual: YMMV


Posted by on 29 March 2019 | Comments (0) | categories: Salesforce

Re-Usable Dynamic Custom Lookup LWC edition


Over at sfdcmonkey there's a nice AURA component that allows for dynamic lookup of a given object. Super nice and useful. I wondered what it would take to be rebuild in LWC

Dynamic Lookup

Same, Same but different

I want to achieve the same functionality, but would accept subtle differences. This is what I got:

  • The original component shows the object icon on the left. My version shows the search symbol that comes out of the box with lightning-input
  • I use 3 components instead of two. Input fields that trigger network calls are fairly common and it makes sense to debounce the input. So I created c-ux-debounced-input that signals entered data only after a period of 300ms
  • The component dispatches an event when a result has been selected or cleared, so it can be used inside other components
  • For now: it can be directly put on a lightning page in page builder and configured there. Useful for demos and test
  • When you clear the selected object, the result list opens up again, so no second network call is made until you change the input value

Read more

Posted by on 27 March 2019 | Comments (2) | categories: Lightning Salesforce Software

Mapping recordIds to Object Names - Offline edition


Lightning in Communities is "Same Same but different". When you want to build neutral components, you need to know what object you are dealing with

ObjectApiName, ObjectName and recordId

In Lightning Aura components one can use force:hasSObjectName to get access to an attribute sObjectName. In Lightning Web components one uses @api objectApiName. Except neither of those work in Communities.

The workaround is to look at the recordId and use DescribeSObjectResult.getKeyPrefix to map a record to the object name. There's a comprehensive list by David and my version as JSON object.

However, depending on your org, that list might vary. So I created a small component that lists out the objects in your current org. Enjoy:

ObjectIdSpy.cls

public without sharing class ObjectIdSpy {
    @AuraEnabled(cacheable=true)
    public static Map<String,String> getObjectIdMappings(){
        Map<String,String> result = new Map<String,String>();
        Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe();
        for(String key : gd.keySet()) {
            Schema.SObjectType ot = gd.get(key);
            String curPrefix = ot.getDescribe().getKeyPrefix();
            String curName = ot.getDescribe().getName();
            // Fair warning: will omit objects that share the prefix
            result.put(curPrefix, curName);
        }
      return result;
    }
}

objectIdSpy.html

<template>
    <lightning-card title="Object Spy">
        <pre>
{idList}
        </code></pre>
    </lightning-card>
</template>

objectIdSpy.js

import { LightningElement, track, wire } from "lwc";
import idSpy from "@salesforce/apex/ObjectIdSpy.getObjectIdMappings";

export default class ObjectIdSpy extends LightningElement {
  @track idList;

  @wire(idSpy)
  spiedUpon({ error, data }) {
    if (data) {
      this.idList = JSON.stringify(data, null, 2);
    } else if (error) {
      this.idList = JSON.stringify(error, null, 2);
    }
  }
}

objectIdSpy.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata" fqn="ObjectIdSpy">
    <apiVersion>45.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Object Id Spy</masterLabel>
    <description>Generates a JSON object of Id and object names</description>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

As usual YMMV!


Posted by on 20 March 2019 | Comments (0) | categories: Salesforce Singapore

Finding Strings in recursively zipped files


I had an itch to scratch. After using Field Trip (which I like a lot) to determine unused fields, the team managing the external Informatica integration claimed they would need weeks to ensure none of the fields are used in any of their (hundreds) of pipelines.

ZIP inception

My first reaction (OK, the second, first one isn't PC) was: Let's go after the source code and just use an editor of choice to do a find in files. Turns out: not so fast. The source export offered by the team was a zip file with an elaborate directory structure containing, tada, zip files. So each of the pipes would need multiple zip operations.

Itch defined

I needed a tool that would start in a directory with a bunch of zip files, unpack them all. Check for zip files in the unpacked result, unzip these and repeat. Once done, take a list of strings and search for occurrences of those and generate a report which shows the files containing these strings

Itch scratched

I created findstring, a command line tool that takes a directory as starting point unzips what can be unzipped (optional) and searches for the occurrence of strings provided in a text file.

Initially I contemplated to render the output as XML, so the final report could be designed in whatever fashion using XSLT. However following KISS, I ended up using Markdown. I might add the XML option later on.

Recursion

The key piece of the tool is recursion (until you stack overflow ;-) ). Reading a directory and dive into directories found. I could have avoided that using Guava and its fileTraverser, but I like some Inception style coding. The key piece is this:

    private boolean expandSources(final File sourceDir) throws IOException {
        boolean result = false;
        final File[] allFiles = sourceDir.listFiles();

        for (final File f : allFiles) {
            if (f.isDirectory()) {
                result = result || this.expandSources(f);

            } else if (f.getName().endsWith(".zip")) {
                final String newDirName = f.getAbsolutePath().replace(".zip", "");
                final File newTarget = new File(newDirName);

                // Need to scan the new directory too
                if (this.expandFile(f, newTarget)) {
                    result = result || this.expandSources(newTarget);
                }
            }
        }
        return result;

    }

The function will return true as long as there was a zip file to be unzipped. The string finding operation (case insensitive) follows the same approach

Use cases

  • Find field usage in ZIP files. Works with a package downloaded from the meta data api or what Informatica exports
  • Check a source directory (doesn't need to contain zips) for keywords like TODO, FIXME, XXX

The command line syntax is very simple:

java -jar findString.jar -d directory -s strings [-o output]

  • -d,--dir <arg> directory with all zip files
  • -s,--stringfile <arg> Filename with Strings to search, one per line
  • -o,--output <arg> Output file name for report in MD format
  • -nz,--nz Rerun find operation on a ready unzipped structure - good for alternate finds

Limits

In its current form the utility will check for strings in any file short of zip. Zip gets unpacked and the result checked. When your directory contains binary files (e.g. images) it will still look for the string occurrence inside. File extension filters might be a future enhancement (share your opinion).

Files are read into memory. So if your directory contains huge files, you will blow your heap. Source code files hardly pose an issue, so the approach worked for me. Alternatively a scanner could be used, should the need arise.

Go give it a spin and keep in mind: YMMV


Posted by on 16 March 2019 | Comments (1) | categories: Salesforce Singapore

Testing Aura and LWC in a single Test


You drank the CoolAid and noticed that the Aura framework has been archived. You are hell bend to migrate your components.

Regression Test required

Aura components were testable using the Lightning Testing Service, while Lightning Web Components get tested using lwc-jest. These tests are not compatible.

UI-licious to the rescue. UI-licious is a testing framework for UI tests. They use a simple JavaScript syntax to provide testing and a rather clever addressing of elements. Other than Selenium, they don't rely on CSS selectors or XPath expressions (You still can use those).

To be very clear: A UI level testing library is not a replacement for proper unit testing. UI-licious has two use cases here: top of the pyramid UI testing and spotting UI level regressions. To learn more about the "testing pyramid", check out Martin Fowler's essay.

To give it a try I created 2 components with identical functionality: one in Aura, one as LWC. The components show a dialog where you can pick values for radio buttons. Shi Ling, the CEO provided the test script (the login subroutine omitted for brevity):

I.wait(30) // wait for salesforce to be ready
I.click("App Launcher")
I.click("Clown around")

I.see("Having 2 components of the same type")

test("The aura version")
test("The LWC version")

function test(btn){
  I.click(btn)
  I.see("Pick an Opportunity and Color")
  I.click("Product Opportunity")
  I.click("Red")
  I.click("Select")
  I.see("Nicely done")
}  

Watch the result for yourself:

What I really like: UI-licious builds the collaboration feature around testing, so stakeholders can see any time what's going on. Give them a try!


Posted by on 14 March 2019 | Comments (0) | categories: Lightning Salesforce WebComponents

Navigation in Lightning communities


In a recent project we had to design navigation in a Lightning community. This is what we learned along the way.

Beyond the menu

When you pick a Lightning Template you will have a build in menu navigation. This works well if all menu items are meant for all users (no assignment of audience), but breaks down for more sophisticated or programmatic navigation.

On first view the Lightning navigation service (available in Aura or LWC) seems like the answer. However on inspection of lightning-navigation you find as supported experiences only Lightning Experience and Salesforce mobile app, Communities are missing.

Digging a little deeper and checking the Page Reference Types, you will find "limited support for Communities". I tested it out, here are my findings:

  • The documentation is accurate. What is stated as working works, what is stated as not supported in communities does not work.
  • The painfully missing piece is standard__component which would allow to navigate to a custom lightning component. It is the only component that supports state (more on that later)
  • Navigate to standard__objectPage opens the list/page layout based on the user's profile. When you specify. actionName="new", the standard object detail page will open. It will not use an eventual define new button overwrite
  • Works as specified: standard__recordPage, standard__knowledgeArticlePage
  • Doesn't work: standard__webPage
  • None of the navigation working in communities supports the state properties
  • The most interesting navigation in communities is standard__namedPage. Beside the predefined default pages "Home","Account management", "Contact Support", "Error", "Top Articles" and "Topic Catalog", it supports "Custom Pages". In other words: any of the pages you have created in your community. So the missing standard_component can be mitigated by embedding it into a custom page. Keep in mind: the pageName property is the URL, not the name.

Transferring state

As mentioned above, the state property gets ignored, dropped without an error when used with any of the working navigation items. The remedy for that is to use the session store. An Aura code snippet would looks like this:

function(component, event, helper) {
    event.preventDefault();
    var navService = component.find("navService");
    var pageReference = {
        type: "standard__namedPage",
        attributes: {
            pageName: "some-page-name"
        },
        state: {
            bingo: true,
            answer: 42,
            tango: "double"
        }
    };
    sessionStorage.setItem('localTransfer', JSON.stringify(pageReference.state));
    navService.navigate(pageReference);
}

I left the state in the pageReference JSON object to show that it doesn't harm. The navService component is defined as <lightning:navigation aura:id="navService"/> in Aura. On the receiving end you use:

var localStuff = sessionStorage.getItem('localTransfer');
if (localStuff) {
 var state = JSON.parse(localStuff);
 // Do the needed stuff here
}

As usual YMMV


Posted by on 12 March 2019 | Comments (2) | categories: Lightning Salesforce

Using render() in LWC


Whatever template system you use, you will end up with show/hide logic based on your data's values. In Aura components you have an expression language (reminded me of JSF), in LWC external (in your JavaScript class) boolean values or functions.

Keep it tidy

A common interaction pattern, similar to the Salesforce default behavior when you have more than one record type available, is to show a pre-selection (which record type), a main selection (required data) and (eventually) a post-selection (what's next?).

In a lightning web component you can handle that easily using if:true|false inside your html template.

But what if the sections are quite lengthy? Maintaining the HTML template can get messy. Enter the render() method. In LWC this method doesn't to the actual rendering, but determines what template to use to render the component.

There are a few simple rules:

  • You need to import your template into your JavaScript file
  • The call to render() must return the imported variable (see example below)
  • You can make the computation dependent on anything inside the class
  • You can't assemble the template in memory as a String, it will throw you an error

Read more

Posted by on 04 March 2019 | Comments (2) | categories: Lightning Salesforce WebComponents

Global value providers in LWC


Drinking the new CoolAid one has to come to terms with the old ways. We had a first glimpse before.

Same but different, reloaded

When developing lightning components using the Aura Framework you could use a series of global value providers that give you access to various data sets: $ContentAsset, $Labels, $Locale, and $Resource.

While this convenient, it pollutes the global name space and it a very proprietary (albeit popular at its time) way to provide information. LWC fixes this in a very standard compliant way. This became possible thanks to the new capabilities in the JavaScript ES6 standard.

In LWC all information provided by Salesforce gets added using ES6 import statements from the @salesforce name space. While that syntax is new to Salesforce developers, it is old news for the rest of JavaScript land. So here you go:

  • $ContentAsset -> import assetName from @salesforce/contentAssetUrl/[AssetName]
  • $Labels -> import labelName from @salesforce/label/[LabelName]
  • $Locale -> import i18nproperty from @salesforce/i18n/[internationalizationProperty] (with various values)
  • $Resource -> import resourceName from @salesforce/resourceUrl/[resourceName]
  • current User Id -> import userId from @salesforce/User/Id

The @salesforce name space provides access to additional data, like apex and schema.

As usual YMMV


Posted by on 01 March 2019 | Comments (1) | categories: Lightning Salesforce WebComponents