tyoshikawa1106のブログ

- Force.com Developer Blog -

SFDC:Apexで『Content-Type: multipart/form-data』のAPIを実行

ApexではHttpRequestをつかって外部APIを実行することができます。『Content-Type: multipart/form-data』の実行がうまくいかずに困っていたのですが、実行方法を教えてもらったのでメモ。


下記のcurlコマンドで実行できる処理があります。

$ curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: multipart/form-data;" -F "grant_type=password" -F “client_id=[sample]" -F "client_secret=[sample]" -F "username=[sample]" -F "password=[sample]" "http://[sample]”


これをApexから実行するとこうなります。

// Body
String boundary = '------------' + String.valueOf(DateTime.now().getTime());
String body  = '';
body += this.create_multi_param(boundary, 'grant_type', 'password');
body += this.create_multi_param(boundary, 'name', '[sample]');
body += this.create_multi_param(boundary, 'client_id', '[sample]');
body += this.create_multi_param(boundary, 'client_secret', '[sample]');
body += this.create_multi_param(boundary, 'username', '[sample]');
body += this.create_multi_param(boundary, 'password', '[sample]');
body += '--' + boundary + '--\r\n';

Http http = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Cache-Control', 'no-cache');
req.setHeader('Content-Length', String.valueOf(body.length()));
req.setHeader('Content-Type', 'multipart/form-data; boundary='+ boundary);
req.setEndpoint('http://[sample]');
req.setBody(body);
req.setMethod('POST');
HttpResponse res = http.send(req);
System.debug(res.getBody());

「create_multi_param」メソッドの処理はこんな感じ。

private String create_multi_param(String boundary, String name, String val){
    String param = '--' + boundary+'\r\n'
    + 'Content-Disposition: form-data; name="' + name + '"'
    + '\r\n\r\n' + val + '\r\n';
    return param;
}


これで『Content-Type: multipart/form-data』のAPIを実行できました。boundary変数に「----」やシステム日時をセットしていますがこれはユニークな文字列を生成するためのものです。これを区切り文字として使用すればいいとのことです。

その他のContent-TypeのAPI

GET処理の場合

Http http = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Cache-Control', 'no-cache');
req.setHeader('Authorization', 'OAuth ' + <token>);
req.setHeader('Content-Type', 'application/json; charset=utf-8');
req.setEndpoint('http://<sample>');
req.setMethod('GET');
HttpResponse res = http.send(req);


POST処理の場合

// API実行
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setHeader('Cache-Control', 'no-cache');
req.setHeader('Authorization', 'OAuth ' + <token>);
req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
req.setEndpoint('http://<sample>');
req.setMethod('POST');
req.setBody(body);
HttpResponse res = http.send(req);


こんな感じです。

Apexコールアウトのサンプル

参考

こちらのリンクも教えてもらったのでメモ

SFDC:Platform Eventsを試してみました

Trailheadを見ながらPlatform Eventsの機能を試してみました。

Platform Eventsの作成

設定で「Platform Events」と検索。New Platform Eventボタンをクリックします。
f:id:tyoshikawa1106:20170717171844p:plain


ラベルや説明項目を入力します。
f:id:tyoshikawa1106:20170717172939p:plain


拡張子は__eとなりました。
f:id:tyoshikawa1106:20170717173013p:plain


検証用に下記項目を作成します。(詳細はTrailheadを確認のこと)
f:id:tyoshikawa1106:20170717173624p:plain

ReplayIdシステムのフィールドとイベントの保持

Salesforceはプラットフォームイベントを24時間保存します。Apexではなく、APIクライアントで保存されたイベントを取得できます。各イベントレコードには、ReplayIDというフィールドが含まれています。このフィールドは、イベントが発行された後にシステムに取り込まれます。各リプレイIDは、前のイベントのIDよりも高いことが保証されていますが、連続するイベントでは必ずしも連続している必要はありません。格納されているすべてのイベントを取得することも、取得したイベントのベースラインとしてイベントのリプレイIDを指定することもできます。

Salesforceはイベントレコードを一時的に保持していますが、SOQLまたはSOSLを使用してイベントレコードを照会することはできません。同様に、レポート、リストビュー、および検索のユーザーインターフェイスでイベントレコードを使用することはできません。CometDを購読し、ReplayIdオプションを使用している場合にのみ、過去のイベントを取得できます。次のユニットのイベントを購読する方法を示します。

イベントを公開する

アプリケーションがSalesforceプラットフォームにある場合は、Apexメソッドを使用するか、Process BuilderやCloud Flow Designerなどの宣言ツールを使用してイベントをパブリッシュできます。アプリが外部アプリの場合、Salesforce APIを使用してイベントを公開できます。

Apexを使用してイベントメッセージを発行する

設定画面でPlatform Eventsを作成後はApexで処理を行えばいいみたいです。

// Create an instance of the event and store it in the newsEvent variable
Cloud_News__e newsEvent = new Cloud_News__e(
           Location__c='Mountain City', 
           Urgent__c=true, 
           News_Content__c='Lake Road is closed due to mudslides.');

// Call method to publish events
Database.SaveResult sr = EventBus.publish(newsEvent);

// Inspect publishing result 
if (sr.isSuccess()) {
    System.debug('Successfully published event.');
} else {
    for(Database.Error err : sr.getErrors()) {
        System.debug('Error returned: ' +
                     err.getStatusCode() +
                     ' - ' +
                     err.getMessage());
    }
}
複数のイベントを公開する場合はこちら
// List to hold event objects to be published.
List<Cloud_News__e> newsEventList = new List<Cloud_News__e>();
// Create event objects.
Cloud_News__e newsEvent1 = new Cloud_News__e(
           Location__c='Mountain City', 
           Urgent__c=true, 
           News_Content__c='Lake Road is closed due to mudslides.');
Cloud_News__e newsEvent2 = new Cloud_News__e(
           Location__c='Mountain City', 
           Urgent__c=false, 
           News_Content__c='Small incident on Goat Lane causing traffic.');
// Add event objects to the list.
newsEventList.add(newsEvent1);
newsEventList.add(newsEvent2);

// Call method to publish events.
List<Database.SaveResult> results = EventBus.publish(newsEventList);

// Inspect publishing result for each event
for (Database.SaveResult sr : results) {
    if (sr.isSuccess()) {
        System.debug('Successfully published event.');
    } else {
        for(Database.Error err : sr.getErrors()) {
            System.debug('Error returned: ' +
                        err.getStatusCode() +
                        ' - ' +
                        err.getMessage());
        }
    }       
}
プロセスビルダーをつかった公開

f:id:tyoshikawa1106:20170717204911p:plain

Salesforce APIを使用してイベントメッセージを公開

sObject RESTエンドポイント:

/services/data/v40.0/sobjects/Cloud_News__e/

POSTリクエストの本文をリクエストする:

{
   "Location__c" : "Mountain City",
   "Urgent__c" : true,
   "News_Content__c" : "Lake Road is closed due to mudslides."
}

プラットフォーム・イベント・レコードが作成されると、REST応答はこの出力のようになります。ヘッダーは簡潔にするために削除されます。

HTTP/1.1 201 Created 

{   
   "id" : "e00xx000000000B",
   "success" : true,
   "errors" : [ ],
   "warnings" : [ ] 
}

Apexから実行

試しにApexから実行してみました。
f:id:tyoshikawa1106:20170717205910p:plain


結果はこちら
f:id:tyoshikawa1106:20170717205952p:plain


ひとまず正常にPlatform Eventsを公開できました。

Platform Eventsの購読

Apexトリガをつかって通知を確認するそうです。

// Trigger for listening to Cloud_News events.
trigger CloudNewsTrigger on Cloud_News__e (after insert) {    
    System.debug('Apex Trigger Go!');
    // List to hold all cases to be created.
    List<Case> cases = new List<Case>();
    
    // Get queue Id for case owner
    //Group queue = [SELECT Id FROM Group WHERE Name='Regional Dispatch' LIMIT 1];
       
    // Iterate through each notification.
    for (Cloud_News__e event : Trigger.New) {
        if (event.Urgent__c == true) {
            // Create Case to dispatch new team.
            Case cs = new Case();
            cs.Priority = 'High';
            cs.Subject = 'News team dispatch to ' + 
                event.Location__c;
            //cs.OwnerId = queue.Id;
            cs.OwnerId = UserInfo.getUserId();
            cases.add(cs);
        }
   }
    
    // Insert all cases corresponding to events received.
    insert cases;
}

f:id:tyoshikawa1106:20170717210423p:plain


ケースを作成するトリガを用意した後に、先程のApexをつかったPlatform EventsのINSERT処理を実行します。するとApexトリガが実行されて無事にケースが登録されました。
f:id:tyoshikawa1106:20170717211201p:plain


使い方はこんな感じで良いみたいです。

Apexテスト

専用のテストの書き方が用意されています。
f:id:tyoshikawa1106:20170717211327p:plain

f:id:tyoshikawa1106:20170717211348p:plain

@isTest
public class PlatformEventTest {
    @isTest static void test1() {
        // Create test event instance
        Cloud_News__e newsEvent = new Cloud_News__e(
            Location__c='Mountain City', 
            Urgent__c=true, 
            News_Content__c='Test message.');
        
        Test.startTest();

        // Call method to publish events
        Database.SaveResult sr = EventBus.publish(newsEvent);
        
        Test.stopTest();
        
        // Perform validation here
        // Check that the case that the trigger created is present.
        List<Case> cases = [SELECT Id FROM Case];
        // Validate that this case was found.
        // There is only one test case in test context.
        System.assertEquals(1, cases.size());
    }
}

CometDでプラットフォームイベント通知を購読する

f:id:tyoshikawa1106:20170717211439p:plain

f:id:tyoshikawa1106:20170717211457p:plain

f:id:tyoshikawa1106:20170717211521p:plain


この辺は試していないのでTrailhead要確認という感じです。


Platform Eventsの使い方はこんな感じでした。

SFDC:UX Prototyping Basics

TrailheadにUX Prototyping Basicsのモジュールが用意されています。このモジュールでは開発前にプロトタイプの実装を行うことのメリットが紹介されていました。またSalesforce社ではどのようにこういった作業をおこなっているかも触れられていました。


Lightning PageとLightning Componentを作りながらプロトタイプ作成→本実装までの流れを確認することができます。次のような画面を開発することができました。
f:id:tyoshikawa1106:20170717122008p:plain

VerticalNavigation.cmp
<aura:component implements="force:appHostable,
flexipage:availableForAllPageTypes,flexipage:availableForRecordHome,
force:hasRecordId,forceCommunity:availableForAllPageTypes,
force:lightningQuickAction" access="global" >
  <div class="slds-grid slds-grid--vertical slds-navigation-list--vertical">
    <h2 class="slds-text-title--caps slds-p-around--medium" id="entity-header">Results for 'CO'</h2>
    <ul>
      <li class="slds-is-active">
        <a href="javascript:void(0);" class="slds-navigation-list--vertical__action slds-text-link--reset" aria-describedby="entity-header">
          All
        </a>
      </li>
      <li>
        <a href="javascript:void(0);" class="slds-navigation-list--vertical__action slds-text-link--reset" aria-describedby="entity-header">
          Accounts
        </a>
      </li>
      <li>
        <a href="javascript:void(0);" class="slds-navigation-list--vertical__action slds-text-link--reset" aria-describedby="entity-header">
          Contacts
        </a>
      </li>
      <li>
        <a href="javascript:void(0);" class="slds-navigation-list--vertical__action slds-text-link--reset" aria-describedby="entity-header">
          Leads
        </a>
      </li>
    </ul>
  </div>
</aura:component>
ResultSection.cmp
  <aura:component controller="SearchResultsController" implements="force:appHostable,
  flexipage:availableForAllPageTypes,
  flexipage:availableForRecordHome,force:hasRecordId,
  forceCommunity:availableForAllPageTypes,
  force:lightningQuickAction" access="global" >
    <aura:attribute name="accounts" type="Account[]"/>
    <aura:attribute name="contacts" type="Contact[]"/>
    <aura:attribute name="leads" type="Lead[]"/>

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

    <div>
      <h2 class="slds-text-heading--medium slds-p-vertical--medium">Accounts</h2>
      <div class="slds-grid">
        <ul class="slds-col slds-size--1-of-1">
          <aura:iteration items="{!v.accounts}" var="account">
            <li class="slds-size--1-of-3 slds-show--inline-block">
              <lightning:card variant="narrow" iconName="standard:account" class="slds-m-around--small">
                <aura:set attribute="title">
                  {!account.Name}
                </aura:set>
                <div class="slds-tile slds-p-horizontal--large">
                  <div class="slds-tile__detail slds-text-body--small">
                    <dl class="slds-list--horizontal slds-wrap">
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="First Label">Phone:</dt>
                      <dd class="slds-item--detail slds-truncate">{!account.Phone}</dd>
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Second Label">Website:</dt>
                      <dd class="slds-item--detail slds-truncate">{!account.Website}</dd>
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Third Label">Account Owner:</dt>
                      <dd class="slds-item--detail slds-truncate">{!account.OwnerId}</dd>
                    </dl>
                  </div>
                </div>
              </lightning:card>
            </li>
          </aura:iteration>
        </ul>
      </div>
      <h2 class="slds-text-heading--medium slds-p-vertical--medium">Contacts</h2>
      <div class="slds-grid">
        <ul class="slds-col slds-size--1-of-1">
          <aura:iteration items="{!v.contacts}" var="contact" indexVar="index">
            <li class="slds-size--1-of-3 slds-show--inline-block">
              <lightning:card variant="narrow" iconName="standard:contact" class="slds-m-around--small">
                <aura:set attribute="title">
                  {!contact.Name}
                </aura:set>
                <div class="slds-tile slds-p-horizontal--large">
                  <div class="slds-tile__detail slds-text-body--small">
                    <dl class="slds-list--horizontal slds-wrap">
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="First Label">Email Address:</dt>
                      <dd class="slds-item--detail slds-truncate">{!contact.Email}</dd>
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Second Label">Title:</dt>
                      <dd class="slds-item--detail slds-truncate">{!contact.Title}</dd>
                      <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Third Label">Phone:</dt>
                      <dd class="slds-item--detail slds-truncate">{!contact.Phone}</dd>
                    </dl>
                  </div>
                </div>
              </lightning:card>
            </li>
          </aura:iteration>
        </ul>
      </div>
      <h2 class="slds-text-heading--medium slds-p-vertical--medium">Leads</h2>
      <div class="slds-grid">
        <ul class="slds-col slds-size--1-of-1"> <aura:iteration items="{!v.leads}" var="lead" indexVar="index">
          <li class="slds-size--1-of-3 slds-show--inline-block">
            <lightning:card variant="narrow" iconName="standard:lead" class="slds-m-around--small">
              <aura:set attribute="title">
                {!lead.Name}
              </aura:set>
              <div class="slds-tile slds-p-horizontal--large">
                <div class="slds-tile__detail slds-text-body--small">
                  <dl class="slds-list--horizontal slds-wrap">
                    <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Second Label">Company:</dt>
                    <dd class="slds-item--detail slds-truncate">{!lead.Company}</dd>
                    <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Second Label">Email Address:</dt>
                    <dd class="slds-item--detail slds-truncate">{!lead.Email}</dd>
                    <dt class="slds-item--label slds-text-color--weak slds-truncate" title="Third Label">Status:</dt>
                    <dd class="slds-item--detail slds-truncate">{!lead.Status}</dd>
                    <dt class="slds-item--label slds-text-color--weak slds-truncate" title="First Label">Phone:</dt>
                    <dd class="slds-item--detail slds-truncate">{!lead.Phone}</dd>
                  </dl>
                </div>
              </div>
            </lightning:card>
          </li>
        </aura:iteration>
      </ul>
    </div>
  </div>
</aura:component>
ResultSectionController.js
({
  doInit : function(component, event, helper) {
    helper.getAccounts(component);
    helper.getContacts(component);
    helper.getLeads(component);
  }
})
ResultSectionHelper.js
({
  getAccounts : function(cmp) {
    var action = cmp.get("c.getAccounts");
    action.setCallback(this, function(response){
      var state = response.getState();
      if (state === "SUCCESS") {
        cmp.set("v.accounts", response.getReturnValue());
      }
    });
    $A.enqueueAction(action);
  },

  getContacts : function(cmp) {
    var action = cmp.get("c.getContacts");
    action.setCallback(this, function(response){
      var state = response.getState();
      if (state === "SUCCESS") {
        cmp.set("v.contacts", response.getReturnValue());
      }
    });
    $A.enqueueAction(action);
  },

  getLeads : function(cmp) {
    var action = cmp.get("c.getLeads");
    action.setCallback(this, function(response){
      var state = response.getState();
      if (state === "SUCCESS") {
        cmp.set("v.leads", response.getReturnValue());
      }
    });
    $A.enqueueAction(action);
  },
})
SearchResultsController
  public with sharing class SearchResultsController {
  @AuraEnabled
  public static List<Account> getAccounts() {
    List<Account> accounts = [SELECT Id, Name, Phone, Website, OwnerId FROM Account LIMIT 5];
    return accounts;
  }
  @AuraEnabled
  public static List<Contact> getContacts() {
    List<Contact> contacts = [SELECT Id, Name, Phone, Email, Title FROM Contact LIMIT 5];
    return contacts;
  }
  @AuraEnabled
  public static List<Lead> getLeads() {
    List<Lead> leads = [SELECT Id, Name, Company, Email, Status, Phone FROM Lead LIMIT 5];
    return leads;
  }
}


ひさしぶりにLightingコンポーネントとLightning Pageをさわりましたが、lightning:cardタグなど開発がやりやすくなっているようでした。
f:id:tyoshikawa1106:20170717122235p:plain

SFDC:Lightning Data Serviceを試してみました

Lightning Data ServiceはLightningコンポーネント開発で利用できる便利な機能です。VisualforceのStandardControllerと同じような感じでApexクラスを用意せずに値を取得したりできます。

レコードの読み込み

ldsDisplayRecord.cmp
<aura:component implements="flexipage:availableForRecordHome, force:hasRecordId">
  <!--inherit recordId attribute-->
  <aura:attribute name="record" type="Object" 
    description="The record object to be displayed"/>
  <aura:attribute name="simpleRecord" type="Object" 
    description="A simplified view record object to be displayed"/>
  <aura:attribute name="recordError" type="String" 
    description="An error message bound to force:recordData"/>

  <force:recordData aura:id="record"
    layoutType="FULL"
    recordId="{!v.recordId}"
    targetError="{!v.recordError}"
    targetRecord="{!v.record}"
    targetFields ="{!v.simpleRecord}"
    mode="VIEW"/>

  <!-- Display a header with details about the record -->
  <div class="slds-page-header" role="banner">
    <p class="slds-text-heading--label">{!v.simpleRecord.Name}</p>
    <h1 class="slds-page-header__title slds-m-right--small
      lds-truncate slds-align-left">{!v.simpleRecord.BillingCity}, 
      {!v.simpleRecord.BillingState}</h1>
  </div>

  <!-- Display Lightning Data Service errors, if any -->
  <aura:if isTrue="{!not(empty(v.recordError))}">
    <div class="recordError">
      <ui:message title="Error" severity="error" closable="true">
        {!v.recordError}
      </ui:message>
    </div>
  </aura:if>
</aura:component>

レコードの保存

ldsSaveRecord.cmp
<aura:component implements="flexipage:availableForRecordHome, force:hasRecordId">

  <!--inherit recordId attribute-->
  <aura:attribute name="record" type="Object" />
  <aura:attribute name="simpleRecord" type="Object" />
  <aura:attribute name="recordError" type="String" />

  <force:recordData aura:id="recordEditor"
    layoutType="FULL"
    recordId="{!v.recordId}"
    targetError="{!v.recordError}"
    targetRecord="{!v.record}"
    targetFields ="{!v.simpleRecord}"
    mode="EDIT" />

  <!-- Display a header with details about the record -->
  <div class="slds-form--stacked">
    <div class="slds-form-element">
      <label class="slds-form-element__label" for="recordName">Name: </label>
      <div class="slds-form-element__control">
        <ui:outputText class="slds-input" aura:id="recordName"
          value="{!v.simpleRecord.Name}"/>
        </div>
    </div>
  </div>

  <!-- Display Lightning Data Service errors, if any -->
  <aura:if isTrue="{!not(empty(v.recordError))}">
    <div class="recordError">
      <ui:message title="Error" severity="error" closable="true">
          {!v.recordError}
      </ui:message>
    </div>
  </aura:if>

  <!-- Display an editing form -->
  <lightning:input aura:id="recordName" name="recordName" label="Name"
    value="{!v.simpleRecord.Name}" required="true"/>

   <lightning:button label="Save Record" onclick="{!c.handleSaveRecord}"
       variant="brand" class="slds-m-top--medium"/>
</aura:component>
LdsSaveRecordController.js
({
    handleSaveRecord: function(component, event, helper) {
        component.find("recordEditor").saveRecord($A.getCallback(function(saveResult) {
            if (saveResult.state === "SUCCESS" || saveResult.state === "DRAFT") {
                console.log("Save completed successfully.");
            } else if (saveResult.state === "INCOMPLETE") {
                console.log("User is offline, device doesn't support drafts.");
            } else if (saveResult.state === "ERROR") {
                console.log('Problem saving record, error: ' + 
                           JSON.stringify(saveResult.error));
            } else {
                console.log('Unknown problem, state: ' + saveResult.state + ', error: ' + JSON.stringify(saveResult.error));
            }
        }));}
})

レコードの作成

ldsNewRecord.cmp
<aura:component implements="flexipage:availableForRecordHome, force:hasRecordId">

  <aura:attribute name="newContact" type="Object"/>
  <aura:attribute name="simpleNewContact" type="Object"/>
  <aura:attribute name="newContactError" type="String"/>

  <force:recordData aura:id="contactRecordCreator"
    layoutType="FULL"
    targetRecord="{!v.newContact}"
    targetFields ="{!v.simpleNewContact}"
    targetError="{!v.newContactError}"
    />

  <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

  <!-- Display a header -->
  <div class="slds-page-header" role="banner">
      <p class="slds-text-heading--label">Create Contact</p>
  </div>

  <!-- Display Lightning Data Service errors -->
  <aura:if isTrue="{!not(empty(v.newContactError))}">
    <div class="recordError">
      <ui:message title="Error" severity="error" closable="true">
        {!v.newContactError}
      </ui:message>
    </div>
  </aura:if>

  <!-- Display the new contact form -->
  <div class="slds-form--stacked">
    <lightning:input aura:id="contactField" name="firstName" label="First Name"
     value="{!v.simpleNewContact.FirstName}" required="true"/>
      
    <lightning:input aura:id="contactField" name="lastname" label="Last Name"
      value="{!v.simpleNewContact.LastName}" required="true"/>
                
    <lightning:input aura:id="contactField" name="title" label="Title"
      value="{!v.simpleNewContact.Title}" />
                      
    <lightning:button label="Save contact" onclick="{!c.handleSaveContact}"
     variant="brand" class="slds-m-top--medium"/>
 </div>
 
</aura:component>
ldsNewRecordController.js
({
    doInit: function(component, event, helper) {
        // Prepare a new record from template
        component.find("contactRecordCreator").getNewRecord(
            "Contact", // sObject type (entityAPIName)
            null,      // recordTypeId
            false,     // skip cache?
            $A.getCallback(function() {
                var rec = component.get("v.newContact");
                var error = component.get("v.newContactError");
                if(error || (rec === null)) {
                    console.log("Error initializing record template: " + error);
                }
                else {
                    console.log("Record template initialized: " + rec.sobjectType);
                }
            })
        );
    },

    handleSaveContact: function(component, event, helper) {
        if(helper.validateContactForm(component)) {
            component.set("v.simpleNewContact.AccountId", component.get("v.recordId"));
            component.find("contactRecordCreator").saveRecord(function(saveResult) {
                if (saveResult.state === "SUCCESS" || saveResult.state === "DRAFT") {
                    // record is saved successfully
                    var resultsToast = $A.get("e.force:showToast");
                    resultsToast.setParams({
                        "title": "Saved",
                        "message": "The record was saved."
                    });
                    resultsToast.fire();

                } else if (saveResult.state === "INCOMPLETE") {
                    // handle the incomplete state
                    console.log("User is offline, device doesn't support drafts.");
                } else if (saveResult.state === "ERROR") {
                    // handle the error state
                    console.log('Problem saving contact, error: ' + 
                                 JSON.stringify(saveResult.error));
                } else {
                    console.log('Unknown problem, state: ' + saveResult.state + ', error: ' + JSON.stringify(saveResult.error));
                }
            });
        }
    }
})

レコードの削除

ldsDeleteRecord.cmp
<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId">

  <aura:attribute name="recordError" type="String" access="private"/>

  <force:recordData aura:id="recordHandler"
    recordId="{!v.recordId}"
    fields="Id"
  />

  <!-- Display Lightning Data Service errors, if any -->
  <aura:if isTrue="{!not(empty(v.recordError))}">
    <div class="recordError">
      <ui:message title="Error" severity="error" closable="true">
        {!v.recordError}
      </ui:message>
    </div>
  </aura:if>

  <!-- Display the delete record form -->
  <div class="slds-form-element">
    <lightning:button
      label="Delete Record"
      onclick="{!c.handleDeleteRecord}"
      variant="brand" />
  </div>

</aura:component>
ldsDeleteRecordController.js ※エラーで動かず
({
handleDeleteRecord: function(component, event, helper) {
    component.find("recordHandler").deleteRecord($A.getCallback(function(deleteResult) {
        if (deleteResult.state === "SUCCESS" || deleteResult.state === "DRAFT") {
            console.log("Record is deleted.");
        }
        else if (deleteResult.state === "INCOMPLETE") {
            console.log("User is offline, device doesn't support drafts.");
        }
        else if (deleteResult.state === "ERROR") {
            console.log('Problem deleting record, error: ' +
                        JSON.stringify(deleteResult.error));
        }
        else {
            console.log('Unknown problem, state: ' + deleteResult.state +
                        ', error: ' + JSON.stringify(deleteResult.error));
        }
    }));
})

非同期レコードの保存

Salesforce1アプリの場合は一時的にオフラインになった場合でも非同期で保存処理を実行する仕組みがあるそうです。
f:id:tyoshikawa1106:20170716185446p:plain

開発ドキュメント







実際に動くコード

上記の開発者ドキュメントページのサンプルコードに取引先責任者の保存機能がありましたので試してみました。

ldsQuickContact.cmp
<aura:component implements="force:lightningQuickActionWithoutHeader,force:hasRecordId">

    <aura:attribute name="account" type="Object"/>
    <aura:attribute name="simpleAccount" type="Object"/>
    <aura:attribute name="accountError" type="String"/>
    <force:recordData aura:id="accountRecordLoader"
        recordId="{!v.recordId}"
        fields="Name,BillingCity,BillingState"
        targetRecord="{!v.account}"
        targetFields="{!v.simpleAccount}"
        targetError="{!v.accountError}"
    />

    <aura:attribute name="newContact" type="Object" access="private"/>
    <aura:attribute name="simpleNewContact" type="Object" access="private"/>
    <aura:attribute name="newContactError" type="String" access="private"/>
    <force:recordData aura:id="contactRecordCreator"
        layoutType="FULL"
        targetRecord="{!v.newContact}"
        targetFields="{!v.simpleNewContact}"
        targetError="{!v.newContactError}"
        />

    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>

    <!-- Display a header with details about the account -->
    <div class="slds-page-header" role="banner">
        <p class="slds-text-heading--label">{!v.simpleAccount.Name}</p>
        <h1 class="slds-page-header__title slds-m-right--small
            slds-truncate slds-align-left">Create New Contact</h1>
    </div>

    <!-- Display Lightning Data Service errors, if any -->
    <aura:if isTrue="{!not(empty(v.accountError))}">
        <div class="recordError">
            <ui:message title="Error" severity="error" closable="true">
                {!v.accountError}
            </ui:message>
        </div>
    </aura:if>
    <aura:if isTrue="{!not(empty(v.newContactError))}">
        <div class="recordError">
            <ui:message title="Error" severity="error" closable="true">
                {!v.newContactError}
            </ui:message>
        </div>
    </aura:if>

    <!-- Display the new contact form -->
     <lightning:input aura:id="contactField" name="firstName" label="First Name"
                     value="{!v.simpleNewContact.FirstName}" required="true"/>
  
    <lightning:input aura:id="contactField" name="lastname" label="Last Name"
                  value="{!v.simpleNewContact.LastName}" required="true"/>
            
    <lightning:input aura:id="contactField" name="title" label="Title"
                  value="{!v.simpleNewContact.Title}" />
    
    <lightning:input aura:id="contactField" type="phone" name="phone" label="Phone Number"
                     pattern="^(1?(-?\d{3})-?)?(\d{3})(-?\d{4})$"
                     messageWhenPatternMismatch="The phone number must contain 7, 10, or 11 digits. Hyphens are optional."
                   value="{!v.simpleNewContact.Phone}" required="true"/>
    
    <lightning:input aura:id="contactField" type="email" name="email" label="Email"
                value="{!v.simpleNewContact.Email}" />
        
    <lightning:button label="Cancel" onclick="{!c.handleCancel}" class="slds-m-top--medium" />
    <lightning:button label="Save Contact" onclick="{!c.handleSaveContact}"
               variant="brand" class="slds-m-top--medium"/>
     
    
</aura:component>
ldsQuickContactController.js
({
    doInit: function(component, event, helper) {
        component.find("contactRecordCreator").getNewRecord(
            "Contact", // sObject type (entityApiName)
            null, // recordTypeId
            false, // skip cache?
            $A.getCallback(function() {
                var rec = component.get("v.newContact");
                var error = component.get("v.newContactError");
                if(error || (rec === null)) {
                    console.log("Error initializing record template: " + error);
                }
                else {
                    console.log("Record template initialized: " + rec.sobjectType);
                }
            })
        );
    },

    handleSaveContact: function(component, event, helper) {
        if(helper.validateContactForm(component)) {
            component.set("v.simpleNewContact.AccountId", component.get("v.recordId"));
            component.find("contactRecordCreator").saveRecord(function(saveResult) {
                if (saveResult.state === "SUCCESS" || saveResult.state === "DRAFT") {

                    // Success! Prepare a toast UI message
                    var resultsToast = $A.get("e.force:showToast");
                    resultsToast.setParams({
                        "title": "Contact Saved",
                        "message": "The new contact was created."
                    });

                    // Update the UI: close panel, show toast, refresh account page
                    $A.get("e.force:closeQuickAction").fire();
                    resultsToast.fire();

                    // Reload the view so components not using force:recordData
                    // are updated
                    $A.get("e.force:refreshView").fire();
                }
                else if (saveResult.state === "INCOMPLETE") {
                    console.log("User is offline, device doesn't support drafts.");
                }
                else if (saveResult.state === "ERROR") {
                    console.log('Problem saving contact, error: ' +
                                 JSON.stringify(saveResult.error));
                }
                else {
                    console.log('Unknown problem, state: ' + saveResult.state +
                                ', error: ' + JSON.stringify(saveResult.error));
                }
            });
        }
    },

    handleCancel: function(component, event, helper) {
        $A.get("e.force:closeQuickAction").fire();
    },
})
ldsQuickContactHelper.js
({
    validateContactForm: function(component) {
        var validContact = true;

         // Show error messages if required fields are blank
        var allValid = component.find('contactField').reduce(function (validFields, inputCmp) {
            inputCmp.showHelpMessageIfInvalid();
            return validFields && inputCmp.get('v.validity').valid;
        }, true);

        if (allValid) {
            // Verify we have an account to attach it to
            var account = component.get("v.account");
            if($A.util.isEmpty(account)) {
                validContact = false;
                console.log("Quick action context doesn't have a valid account.");
            }
        	return(validContact);
            
        }  
	}
       
})


『force:lightningQuickActionWithoutHeader』が宣言されているので、クイックアクションで利用できます。
f:id:tyoshikawa1106:20170716190736p:plain


作成したアクションはページレイアウトで追加できます。
f:id:tyoshikawa1106:20170716191001p:plain


動かしてみたらエラーメッセージ。
f:id:tyoshikawa1106:20170716191405p:plain


アレっと思ってコードをちゃんと確認したら取引先のページに設置するためのコンポーネントでした。ということで、取引先アクションとして作り直して・・・
f:id:tyoshikawa1106:20170716191622p:plain


無事に動かすことができました。
f:id:tyoshikawa1106:20170716191732p:plain


保存処理もサクサク動きます。必須項目の値が未入力の場合は入力欄の下にリアルタイムでエラーコメントが表示されたりしました。
f:id:tyoshikawa1106:20170716191850p:plain


シンプルなLightningコンポーネントの開発なら少ない工数で実装を可能にしてくれそうです。

SFDC:Commerce Cloudの事例サイト一覧

Commerce Cloudの事例サイト一覧が公開されています。

f:id:tyoshikawa1106:20170716111349p:plain

eCommerce Platform for Retailers | Demandware


Commerce Cloudはショッピングサイトを構築できる製品で、多言語化対応やモバイル対応などもサポートするWebサイトを構築できるようです。またEinsteinとの連携で購入情報を分析してオススメの商品を強調したり..的なことができるみたいです。他にも一時的にネットワークに繋がらなくなった時も問題なく利用できるResilient POSという機能があったりするみたいです。


詳細はTrailheadにモジュールが用意されています。

SFDC:GitHubでバージョン管理

TrailheadにGitHubをつかったバージョン管理についてのモジュールが追加されています。GitHubで何ができるかや初期セットアップ、基本的なコマンドの意味、プロジェクトのフローなどが解説されていました。

f:id:tyoshikawa1106:20170716105559p:plain


まだ英語版ですがChromeの日本語翻訳機能を使うとざっくりとした意味を書くにできると思います。
f:id:tyoshikawa1106:20170716105657p:plain

関連