見出し画像

[開発チュートリアル:第二部]入力エリアをカスタマイズしてみる〜Salesforce フロー画面コンポーネントを含んだパッケージを作成しよう〜

はじめに

こんにちは、co-meetingにてエンジニアしているハナミズキです。この記事は、「Salesforce フロー画面コンポーネントを含んだパッケージを作成しよう」の第二部です。

関連するパッケージの紹介:

ここで学習できること

  • フロー画面コンポーネントのカスタムプロパティエディタの作り方

  • UnofficialSFのコンポーネントを取り込みカスタムプロパティエディタをリッチに改変する作り方

カスタムプロパティエディタ(シンプルなUI)の作成

Salesforceでは、画面フロー用のコンポーネントを作成するときに、画面コンポーネントのUIをカスタマイズする方法が存在します。

詳細は以下公式サイトの通りです。
<Flow Builder のアクションと画面コンポーネントの UI のカスタマイズ - Salesforce Lightning Component Library>
https://developer.salesforce.com/docs/component-library/documentation/ja-jp/lwc/lwc.use_flow_custom_property_editor
現状の仕組みだと、オブジェクトを選択した後に手動でオブジェクトのAPI参照名を入力しないといけません。レコードを指定した時に自動算出してくれたら楽になるのに。。と感じませんか?

このような思いはカスタムプロパティエディタを独自に作成することで入力しやすくできます。さっそく作っていきましょう。

カスタムプロパティエディタを追加

ファイル名:cmOutputFieldEditor
フォルダを入力:force-app/main/default/lwc

VSCodeのコマンドパレットを開き(Ctrl+Shift+P/Cmd+Shift+P)、[SFDX: Lightning Web コンポーネントを作成]-["cmOutputFieldEditor"ファイル名を入力]-["force-app/main/default/lwc"ディレクトリを選択]を選択する。もしくは、force-app/main/default/lwcを右クリックして[SFDX: Lightning Web コンポーネントを作成]を選択する。もしくはVSCodeのターミナルを開き(Ctrl+J/Cmd+J)以下のコマンドを実行することで、Lightning Web コンポーネントファイル(.html/.js/.js-meta.xml)が作成されます。

sfdx force:lightning:component:create -n cmOutputFieldEditor -d force-app/main/default/lwc --type lwc

自動生成されたファイルを開き、以下のように内容を変更してください。
lwc/cmOutputFieldEditor/cmOutputFieldEditor.html

<template>
<div class="slds-p-around_xx-small">
  <!-- 入力フォーム:レコード -->
  <lightning-input
    name="record"
    label={inputValues.record.label}
    value={inputValues.record.value}
    type="text"
    onchange={handleChangeRecord}
  >
  </lightning-input>
  <!-- 入力フォーム:オブジェクト (レコードから自動算出したオブジェクト名を表示)-->
  <lightning-input
    disabled
    name="objectName"
    label={inputValues.objectName.label}
    value={inputValues.objectName.value}
    type="text"
  >
  </lightning-input>
  <!-- 入力フォーム:項目名 -->
  <lightning-input
    name="fieldName"
    label={inputValues.fieldName.label}
    value={inputValues.fieldName.value}
    type="text"
    onchange={handleChangeFieldName}
  >
  </lightning-input>
</div>
</template>

カスタムプロパティエディタのHTML側コードです。テキストフィールドとなるlightning-inputを利用して、レコード情報、オブジェクト名、項目名を表示しています。オブジェクト名については入力不可です。​​レコード情報や項目名に関してはテキストフィールドに変更があった際に呼び出すJavaScript側のメソッドを指定します。

lwc/cmOutputFieldEditor/cmOutputFieldEditor.js

import { LightningElement, track, api } from "lwc";

export default class CmOutputFieldEditor extends LightningElement {
 // フロー画面コンポーネントの公開プロパティの値
 _inputVariables = [];
 // フロー画面コンポーネントの公開プロパティのデータ構造
 _genericTypeMappings;
 // フローの要素およびリソースに関すデータ(入力変数の名前と型など)
 @api builderContext = {};

 @track
 inputValues = {
   objectName: {
     value: null,
     valueDataType: "string",
     label: "オブジェクト"
   },
   record: {
     value: null,
     valueDataType: "reference",
     label: "レコード"
   },
   fieldName: {
     value: null,
     valueDataType: "string",
     label: "項目名"
   }
 };

 // フロー画面コンポーネントの公開プロパティの値を取得・設定
 @api
 get inputVariables() {
   return this._inputVariables;
 }
 set inputVariables(variables) {
   this._inputVariables = variables || [];
   this._initializeValues();
 }

 // フロー画面コンポーネントの公開プロパティのデータ構造を取得・設定
 @api get genericTypeMappings() {
   return this._genericTypeMappings;
 }
 set genericTypeMappings(mappings) {
   this._genericTypeMappings = mappings;
   this._initializeObjectName();
 }

 // 公開プロパティの値(_inputVariables)を解析して内部変数inputValuesを初期化
 _initializeValues() {
   this._inputVariables.forEach((variable) => {
     this.inputValues[variable.name] = {
       ...this.inputValues[variable.name],
       value: variable.value,
       valueDataType: variable.valueDataType,
       isCollection: variable.isCollection
     };
   });
 }

 // 公開プロパティのデータ構造(genericTypeMappings)を解析して内部変数inputValuesのオブジェクト値をセット
 _initializeObjectName() {
   const type = this.genericTypeMappings.find(
     ({ typeName }) => typeName === "T"
   );
   this.inputValues.objectName.value = type && type.typeValue;
 }

 // 公開プロパティ「レコード」が変更された時
 // 関連するプロパティ(オブジェクト|項目名)の値変更も含めて、画面フロー側に値とpropertyTypeの変更を通知する
 handleChangeRecord(event) {
   if (event.target && event.detail) {
     const lookupReferenceName = event.detail.value;
     // レコードのobjectNameを取得
     const lookupRecordObjectName = this._getRecordLookupsObjectName(lookupReferenceName);
     if (lookupRecordObjectName !== null) {
       // propertyType name="T"に対する設定
       this._dispatchFlowTypeMappingChangeEvent('T', lookupRecordObjectName);
       // property name="objectName"に対する設定
       this._dispatchFlowValueChangeEvent('objectName', lookupRecordObjectName, 'string');
       // property name="record"に対する設定
       this._dispatchFlowValueChangeEvent('record', lookupReferenceName, 'reference');
     } else {
       this._dispatchFlowValueChangeEvent('objectName', '', 'string');
       this._dispatchFlowValueChangeEvent('record', '', 'reference');
       this._dispatchFlowValueChangeEvent('fieldName', '', 'string');
       this.inputValues.objectName.value = '';
       this.inputValues.fieldName.value = '';
     }
   }
 }

 // 入力されたレコードの変数名をもとにbuilderContextを検索してpropertyType情報を取得
 _getRecordLookupsObjectName(lookupRecordName) {
   if (!this.builderContext.recordLookups) {
     return null;
   }
   const lookupRecord = this.builderContext.recordLookups.find(
     ({ name }) => name === lookupRecordName
   );
   if (lookupRecord) {
     return lookupRecord.object;
   }
   return null;
 }

 // 公開プロパティ「項目名」が変更された時、画面フロー側に状態の変更を通知する
 handleChangeFieldName(event) {
   if (event.detail) {
     this.inputValues.fieldName.value = event.detail.value;
     this._dispatchFlowValueChangeEvent('fieldName', event.detail.value, 'string');
   } else {
     this.inputValues.fieldName.value = '';
     this._dispatchFlowValueChangeEvent('fieldName', '', 'string');
   }
 }

 // 公開プロパティの値が変更されたことを通知する
 _dispatchFlowValueChangeEvent(id, newValue, newValueDataType) {
   const valueChangedEvent = new CustomEvent(
     "configuration_editor_input_value_changed",
     {
       bubbles: true,
       cancelable: false,
       composed: true,
       detail: {
         name: id,
         newValue: newValue ? newValue : null,
         newValueDataType: newValueDataType
       }
     }
   );
   this.dispatchEvent(valueChangedEvent);
 }

 // 公開プロパティのpropertyTypeが変更されたことを通知する
 _dispatchFlowTypeMappingChangeEvent(typeName, typeValue) {
   const typeChangedEvent = new CustomEvent(
     "configuration_editor_generic_type_mapping_changed",
     {
       bubbles: true,
       cancelable: false,
       composed: true,
       detail: {
         typeName,
         typeValue
       }
     }
   );
   this.dispatchEvent(typeChangedEvent);
 }
}

カスタムプロパティエディタのJavaScript側コードです。

lwc/cmOutputFieldEditor/cmOutputFieldEditor.js-meta.xml

<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <isExposed>false</isExposed>
</LightningComponentBundle>

カスタムプロパティエディタを利用するように更新

既存のファイルを開き、以下のように内容を変更してカスタムプロパティエディタを利用するように更新します。

lwc/cmOutputField/cmOutputField.js-meta.xml

<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
   <apiVersion>54.0</apiVersion>
   <masterLabel>CM OutputField</masterLabel>
   <description>Lightning風の項目表示ならこれ!レコードと項目を指定するだけで画面フロー上に標準のLightningと同じ形式で簡単に出力できるコンポーネントです!</description>
   <isExposed>true</isExposed>
   <targets>
     <target>lightning__FlowScreen</target>
   </targets>
   <targetConfigs>
     <!-- configurationEditorにてカスタムプロパティエディタを指定 -->
     <targetConfig targets="lightning__FlowScreen" configurationEditor="c-cm-output-field-editor">
       <propertyType name="T" extends="SObject" label="オブジェクト" />
       <property label="レコード" name="record" role="inputOnly" type="{T}" />
       <property label="オブジェクト名" name="objectName" type="String" role="inputOnly" />
       <property label="項目名(API参照名)" name="fieldName" type="String" role="inputOnly" />
     </targetConfig>
   </targetConfigs>
</LightningComponentBundle>

デプロイして検証しよう

VSCodeのコマンドパレットを開き(Ctrl+Shift+P/Cmd+Shift+P)、[SFDX: デフォルトのスクラッチ組織へソースをプッシュ]を選択する。もしくは、VSCodeのターミナルを開き(Ctrl+J/Cmd+J)以下のコマンドを実行することでスクラッチ組織に作成・更新したソースを追加デプロイします。

sfdx force:source:push -u flow_component_ouput_field_sample_SCRATCH

「デプロイ」の補足に従いpackage.jsonscriptsに、src:pushを追加している場合は、コマンド[npm run src:push]実行でもデプロイできます。
スクラッチ組織を開き、前回作成したフローScreen Flow: Sample Account Displayの画面を確認してみましょう。
入力エリアが、以下のようにカスタムプパティエディタで指定した形に更新されていたらデプロイ成功です。

試しに1つ追加して入力する形を確認してみましょう。
電話を表示したい場合の入力例

  • API 参照名:display_OutputFieldPhone

  • レコード: get_Account

  • 項目名(API参照名):Phone

前回の入力と比べて入力する項目が減ったことが分かりますね。

保存してデバッグ実行してみましょう。問題なく表示されたら、カスタムプロパティエディタの作り込み成功です。

カスタムプロパティエディタを作った事により、以前より入力項目が少なくなりました。入力する項目が少なくなったことで、入力ミスが減り多少使いやすくなったのではないでしょうか。

しかし、シンプルが故に、ただの入力フォームとなってしまっております。

これまでテキストボックスにカーソルを合わせると自動的に以下のようなコンボボックスが出てきて「入力」や「絞り込み検索」「リスト内からの選択」が実行できたのに、利用できません。大問題です。

ユーザーの操作性を上げるには、この便利なコンボボックスと同じように動くよう更にカスタムプロパティエディタを作り込んであげる必要があります。

しかし、上記のような標準と同等の入力フォームを開発しようとするとかなり工数がかかります。よって、次の章では第三者の力を借りてちょっと使い勝手をよくしていきましょう。

カスタムプロパティエディタ(リッチな入力しやすいUI)のバージョンアップ

UnofficialSFでは、この入力フォームを実現するコンポーネントが提供されています。今回はこちらを使いましょう。なお、UnofficialSFはSalesforceの中の人たちが考えた様々なアイデアを紹介しているサイトです。

UnofficialSFのパッケージインストール

最終的に公式ライブラリとして機能が提供してくれれば嬉しいですが、今はまだ提供されておりません。ソースコードを直接取り込み対応することも可能ではありますが、今回は、UnofficialSFのサイトFLOW ACTION AND SCREEN COMPONENT BASEPACKにて公開してくれているパッケージ「FlowActionsBasePack」と「FlowScreenComponentsBasePack」を組織にインストールして使ってみましょう。
今回は以下のバージョンを利用しております。

  • FlowActionsBasePack Version 3.0.0 Managed

  • FlowScreenComponentsBasePack Version 3.0.6 Unmanaged

<補足>
コマンド実行で対応できる今回のようなSFDXプロジェクトは、VSCodeのターミナルを開き(Ctrl+J/Cmd+J)以下のコマンドを実行してインストールすると楽です。

FlowActionsBasePack

sfdx force:package:install --package 04t8b000001Eh4YAAS -u flow_component_ouput_field_sample_SCRATCH --wait 10 --publishwait 10

FlowScreenComponentsBasePack

sfdx force:package:install --package 04t5G000003rUvVQAU -u flow_component_ouput_field_sample_SCRATCH --wait 10 --publishwait 10

コマンドを実行したら、スクラッチ組織を開き、[設定] から、[クイック検索] ボックスに「パッケージ」と入力し、[インストール済みパッケージ]を選択します。
以下のように表示されていたらパッケージのインストール成功です。

パッケージ「FlowScreenComponentsBasePack」の[コンポーネントを表示]をクリックすると複数のコンポーネントがインストールされていることが確認できますが、今回はこの中でも、「fsc_flowCombobox」と「fsc_pickObjectAndField3」を利用します。

既存のカスタムプロパティエディタをUnofficialSFのコードを利用するように更新

fsc_flowComboboxは、カスタムプロパティエディタの公開プロパティautomaticOutputVariablesを利用します。カスタムプロパティエディタが初期化されたり、システム管理者がカスタムプロパティエディタで値を変更したりすると、自動的に保存された値は automaticOutputVariables に渡されます。こちらのプロパティに関する詳細は以下SalesforceSpring'21リリースノートの内容に記載されているので参考にしてください。
自動出力にアクセスする Flow Builder のカスタムプロパティエディタの作成
既存のファイルを開き、以下のように内容を変更してUnofficialSFの機能を利用するように更新します。

lwc/cmOutputFieldEditor/cmOutputFieldEditor.html


<template>
<div class="slds-p-around_xx-small">
  <!-- 入力フォーム:レコード=>利用: Flow Combobox -->
  <c-fsc_flow-combobox
    name="record"
    label={inputValues.record.label}
    value={inputValues.record.value}
    value-type={inputValues.record.valueDataType}
    builder-context-filter-collection-boolean={inputValues.record.isCollection}
    builder-context={builderContext}
    automatic-output-variables={automaticOutputVariables}
    onvaluechanged={handleChangeRecord}
  ></c-fsc_flow-combobox>
  <!-- 入力フォーム:オブジェクト (レコードから自動算出したオブジェクト名を表示)-->
  <lightning-input
    name="objectName"
    disabled
    type="text"
    label={inputValues.objectName.label}
    value={inputValues.objectName.value}
  >
  </lightning-input>
  <!-- 入力フォーム:項目名=>利用: Object and Field Picker -->
  <c-fsc_pick-object-and-field3
    name="fieldName"
    field-label={inputValues.fieldName.label}
    object-type={inputValues.objectName.value}
    field={inputValues.fieldName.value}
    hide-object-picklist="true"
    onfieldselected={handleChangeFieldName}
  ></c-fsc_pick-object-and-field3>
</div>
</template>

lwc/cmOutputFieldEditor/cmOutputFieldEditor.js

import { LightningElement, track, api } from 'lwc';

export default class CmOutputFieldEditor extends LightningElement {
 // フロー画面コンポーネントの公開プロパティの値
 _inputVariables = [];
 // フロー画面コンポーネントの公開プロパティのデータ構造
 _genericTypeMappings;
 // フローの要素およびリソースに関すデータ(入力変数の名前と型など)
 @api builderContext = {};
 // 自動保存された変数のデータ構造が含まれた公開プロパティ【追加】
 @api automaticOutputVariables;

 @track
 inputValues = {
   objectName: {
     value: null,
     valueDataType: 'string',
     isCollection: false, // 追加
     label: 'S オブジェクト種別' // 画面フロー標準のエラーと名前を合わせるために変更
   },
   record: {
     value: null,
     valueDataType: 'reference',
     isCollection: false, // 追加
     label: 'レコード'
   },
   fieldName: {
     value: null,
     valueDataType: 'string',
     isCollection: false, // 追加
     label: '項目名'
   }
 };

 // フロー画面コンポーネントの公開プロパティの値を取得・設定
 @api
 get inputVariables() {
   return this._inputVariables;
 }
 set inputVariables(variables) {
   this._inputVariables = variables || [];
   this._initializeValues();
 }

 // フロー画面コンポーネントの公開プロパティのデータ構造を取得・設定
 @api get genericTypeMappings() {
   return this._genericTypeMappings;
 }
 set genericTypeMappings(mappings) {
   this._genericTypeMappings = mappings;
   this._initializeObjectName();
 }

 // 公開プロパティの値(_inputVariables)を解析して内部変数inputValuesを初期化
 _initializeValues() {
   this._inputVariables.forEach((variable) => {
     this.inputValues[variable.name] = {
       ...this.inputValues[variable.name],
       value: variable.value,
       valueDataType: variable.valueDataType,
       isCollection: variable.isCollection
     };
   });
 }

 // 公開プロパティのデータ構造(genericTypeMappings)を解析して内部変数inputValuesのオブジェクト値をセット
 _initializeObjectName() {
   const type = this.genericTypeMappings.find(
     ({ typeName }) => typeName === 'T'
   );
   this.inputValues.objectName.value = type && type.typeValue;
 }

 // 公開プロパティ「レコード」が変更された時
 // 関連するプロパティ(オブジェクト|項目名)の値変更も含めて、画面フロー側に値とpropertyTypeの変更を通知する
 handleChangeRecord(event) {
   if (event.target && event.detail) {
     const lookupReferenceName = event.detail.newValue;
     // レコードのobjectNameを取得
     const lookupRecordObjectName = this._getRecordLookupsObjectName(lookupReferenceName);
     if (lookupRecordObjectName != null) {
       // propertyType name="T"に対する設定
       this._dispatchFlowTypeMappingChangeEvent('T', lookupRecordObjectName);
       // property name="objectName"に対する設定
       this._dispatchFlowValueChangeEvent('objectName', lookupRecordObjectName, 'string');
       // property name="record"に対する設定
       this._dispatchFlowValueChangeEvent('record', lookupReferenceName, 'reference');
     } else {
       this._dispatchFlowValueChangeEvent('objectName', '', 'string');
       this._dispatchFlowValueChangeEvent('record', '', 'reference');
       this._dispatchFlowValueChangeEvent('fieldName', '', 'string');
       this.inputValues.objectName.value = '';
       this.inputValues.fieldName.value = '';
      }
   }
 }

 // 入力されたレコードの変数名をもとにbuilderContextを検索してpropertyType情報を取得
 _getRecordLookupsObjectName(lookupRecordName) {
   if (!this.builderContext.recordLookups) {
     return null;
   }
   const lookupRecord = this.builderContext.recordLookups.find(
     ({ name }) => name === lookupRecordName
   );
   if (lookupRecord) {
     return lookupRecord.object;
   }
   return null;
 }

 // 公開プロパティ「項目名」が変更された時、画面フロー側に状態の変更を通知する
 handleChangeFieldName(event) {
   if (event.detail) {
     this.inputValues.fieldName.value = event.detail.value;
     this._dispatchFlowValueChangeEvent('fieldName', event.detail.value, 'string');
   } else {
     this.inputValues.fieldName.value = '';
     this._dispatchFlowValueChangeEvent('fieldName', '', 'string');
   }
 }

 // 公開プロパティの値が変更されたことを通知する
 _dispatchFlowValueChangeEvent(id, newValue, newValueDataType) {
   const valueChangedEvent = new CustomEvent(
     'configuration_editor_input_value_changed',
     {
       bubbles: true,
       cancelable: false,
       composed: true,
       detail: {
         name: id,
         newValue: newValue ? newValue : null,
         newValueDataType: newValueDataType
       }
     }
   );
   this.dispatchEvent(valueChangedEvent);
 }

 // 公開プロパティのpropertyTypeが変更されたことを通知する
 _dispatchFlowTypeMappingChangeEvent(typeName, typeValue) {
   const typeChangedEvent = new CustomEvent(
     'configuration_editor_generic_type_mapping_changed',
     {
       bubbles: true,
       cancelable: false,
       composed: true,
       detail: {
         typeName,
         typeValue
       }
     }
   );
   this.dispatchEvent(typeChangedEvent);
 }
}

デプロイして検証してみよう

VSCodeのコマンドパレットを開き(Ctrl+Shift+P/Cmd+Shift+P)、[SFDX: デフォルトのスクラッチ組織へソースをプッシュ]を選択する。もしくは、VSCodeのターミナルを開き(Ctrl+J/Cmd+J)以下のコマンドを実行することでスクラッチ組織に作成・更新したソースを追加デプロイします。

sfdx force:source:push -u flow_component_ouput_field_sample_SCRATCH

「デプロイ」の補足に従いpackage.jsonscriptsに、src:pushを追加している場合は、コマンド[npm run src:push]実行でもデプロイできます。
スクラッチ組織を開き、前回作成したフローScreen Flow: Sample Account Displayの画面を確認してみましょう。
以前入力した内容が以下の形に更新されたらデプロイ成功です。

さらにこの状態で、1つコンポーネントを追加してどのように入力方法が変わったのか確認してみましょう。
年間売上を表示したい場合の入力例

  • API 参照名:display_OutputFieldAnnualRevenue

  • レコード: get_Account

  • 項目名(API参照名):年間売上

前回と違って、テキストボックスをクリックすると選択可能なコンボボックスが表示されるようになりました。

項目名も関連する項目名一覧がコンボボックスのリストへ表示され、API参照名もしくはラベル名を使って絞り込み、選択できるようになりました。キーボード入力が減りさらに入力ミスを防げるUIにカスタマイズ完了です。

保存してデバッグ実行してみましょう。問題なく表示されたら、カスタムプロパティエディタの作り込み成功です。

Next

最後に今回作成したソースコードをパッケージ化しましょう。