TrailheadやSalesforce BlogなどでSalesforce DXについての情報が公開されたようです。
SFDC:Salesforce DXについての情報が公開されました
SFDC:ケースコメントと取引先責任者への通知メール
SalesCloudライセンスで利用できるケースオブジェクトを使えば顧客からの問い合わせを簡単に管理できるようになります。
ケースオブジェクトで使える便利な機能のひとつにケースコメントがあります。
ケースコメントは問い合わせ対応チームで情報共有するのに便利です。作成日や最終更新日も管理されます。
ケースコメントには公開という機能がついています。公開にチェックを付けるとChatterアンサーやコミュニティユーザがケースにアクセスできる環境の場合、そのコメントを顧客が確認することができます。また取引先責任者にメール通知の機能をつかって問い合わせ元の顧客にコメントの内容をメールでお知らせすることも可能です。
ケースコメントは社内での情報共有のみで利用したいので顧客には表示したくない。...という場合もよくあると思います。この通知機能は設定メニューのサポート設定で有効化して初めて機能するようになります。
サポート設定で有効化されていなければ通知機能は実行されません。
取引先責任者にケースコメントを通知する機能を有効化するとメールテンプレートが選択できるようになります。
有効化後は公開チェックとメール送信のチェックが表示されます。
チェックをつけてケースコメントを入力するとメールテンプレートで記載したメッセージが表示されました。
メールテンプレート本文
入力したケースコメントがそのまま顧客に送信されるわけではないようです。社内向けのケースコメントに対してうっかり公開チェックやメール送信チェックをつけてしまってもすぐに非公開にすれば顧客に表示されることはありませんでした。(メールは送信されますので操作ミス自体は注意が必要ですが)
ちなみにメールテンプレートにはケースコメントの差し込みもできます。次のように設定すれば顧客がコメントを確認できるようになります。
問い合わせ管理にケースを利用する場合はサポート設定でどのような通知を行うが考慮し、必要がない場合は通知機能をONにしない状態にしておいた方が良さそうです。
ケースコメントで通知メールを送れることがわかったので、メールテンプレートで差し込める最新のケースコメントが作成日か最終更新日のどちら判断されるか確認してみました。
2件あるケースコメントの内、最初に作成された方を更新して保存します。
どちらの本文が差し込まれるかなと思ったのですが、最終更新日ではなく作成日で判定されました。
ケースコメントの編集は十分考えられますので、メールへの差し込みで利用するのはあまり好ましくなさそうです。
用途や状況によりますが、うっかり操作ミスを考えるとケースコメント入力時にメール送信するよりも、顧客への回答はケースコメントではなく、活動のメール送信機能などを利用した方が良い気がしました。
参考
SFDC:Salesforce Classicへの切り替え無効化
ヘッダーメニューのリンクからLightning ExperienceとSalesforce Classicの切り替えが出来ますが、Lightning ExperienceからClassicへ戻すのを制限したい
ケースがあると思います。その場合はプロファイル設定で制御可能です。
システム管理者権限セクションにSalesforce Classic に切り替えるオプションを非表示のチェックが用意されています。こちらで対応可能でした。
SFDC:Salesforce Trailblazers Communityが公開されました
7月4日のイベント「Trailhead Live」でSalesforce Trailblazers Communityサイトが公開されたとの情報がありました。
日本のコミュニティ向けの情報がこちらのサイトにまとめられているみたいです。
ABOUT
EVENT
GROUP
まだあまり情報は多くありませんが、ユーザ会やイベント日などがこのサイトで確認できるようになるみたいです。
SFDC:プロファイル作成時のPasswordExpiration 項目の値が無効なエラー
プロファイルコピーして別のプロファイルを新規作成しようとした際にPasswordExpiration 項目の値が無効というエラーに遭遇しました。
このエラーですがプロファイルの「パスワードの有効期間」に 180日に設定できない事象が関係するエラーとのことです。
プロファイル設定画面を確認したところ、180日で設定されていました。
解決方法ですが、90日などに変更することで解決するとのことです。この問題はKnown Issues サイトに既に上がっている問題でした。
▼Summer '17 Profile Password Policies do not allow 180 days expiration
Summer '17 Profile Password Policies do not allow 180 days expiration
Known Issues サイトで「This Issue Affects me」をクリックいただきますと、 不具合が修正されたタイミングでお知らせのメールを受け取れるそうなので気になる人はやっておくと良さそうです。
SFDC:プロセスビルダーでChatter投稿機能を実装するときのポイント
Salesforceのプロセスビルダーを利用するとChatterに投稿する機能を簡単に実装できます。
例えばカスタム項目Chatter共有にチェックをつけて保存したときにセールスのグループに共有する機能を実装しました。
こんな感じです。
Chatter投稿の実行条件を作成したタイミングではなく、項目が特定の値で更新されたらとすることで、一度必須項目のみ入力して時間のあるときに詳細を入力しChatterに共有するといったオペレーションが可能になります。
特定の条件が無い場合はチェックボックス型にしておくのがわかりやすくていいと思います。
自動投稿した内容にコメントを入れたりすることで情報共有がやりやすくなります。
ここで注意点です。Chatter共有にチェックをつけて保存するとセールスグループに自動投稿する仕組みが追加してみました。そんな組織でデータの一括更新が行われます。
その結果・・・こんなことになります。
過去に登録された取引先をChatter投稿する必要はなかったのですが、Chatter共有にチェックがある状態で保存すると実行の条件があるせいで大量に不要な投稿が行われてしまいました。
数十件程度だったら笑い話ですが、数百件、数千件でこれをやってしまうと悲しいことになると思います。こんな悲しい事態を発生させないためには、実行条件にある「レコードに指定の変更が行われた場合にのみアクションを実行しますか?」のオプションで「はい」にチェックを付けます。
このチェックをつけることでChatter共有のチェックがFalseからTrueになったタイミングでのみ実行されるようになります。データの一括更新作業に影響がでることはありません。
チェックが付いていたら更新してはいけないなどのルールで回避しようといった方法を考える場合もあるかもしれませんが、管理の引き継ぎなどでそのルールが引き継がれずデータの一括更新が行われてしまうといったリスクが考えられます。
特定の条件で自動実行する処理を実装するときはこの値変更の有無までチェックすることでより安全で使いやすいシステムにできると思います。これはプロセスビルダーだけではなくApexトリガでも同じだと思います。システムを運用していると新しい項目が追加されたりして過去データの更新が必要になることがかならずでてきます。そうしたときに自動実行機能があるから実施できませんということにならないように考慮しておく必要があります。
Chatterグループへの誤投稿の修復方法
残念なことにChatterグループへの大量誤投稿が行われてしまった場合、それを削除する必要があります。1人の作業者が手動で1件ずつ削除していった場合、2時間ぐらいで100〜200件ぐらいは削除できる...かもしれません。なので500件ぐらいなら4〜5時間ぐらいで削除できると思います。ですが実際の所そんな時間と気力がある人はあまりいないと思います。。そこで思いつくのはSOQLとApexをつかって一括削除する方法です。実際に試したことは無いのですがこれは絶対に辞めたほうがいいと思います。もしWHERE条件に失敗して組織のすべての投稿が削除された場合、取り返しがつきません。
カスタムレポートタイプを作成することでChatterグループの誤投稿を抽出するレポートを作成できます。
グループの名前、作成者、作成日などで絞り込むことで絞り込みができると思います。
自動投稿の場合は特定のキーワードが含まれています。それを条件に入れると絞り込みの精度が向上します。
これでレポートを用意できました。
削除するにはレコードIDが必要ですが、必要なのは『フィード投稿 ID』ではなく『フィード項目ID』の項目になります。(※ここまで『フィード投稿 ID』の方でキャプチャを取っていましたが、このタイミングで間違いに気づきました。。)
『0D5』で始まるIDが必要になります。
SOQLクエリでFeedItemオブジェクトの値を見てみるとこちらがレコードIDであることを確認できると思います。
また、フィード項目IDの値はレコードIDになりますのでURLに直接指定すると投稿ページに移動できます。
対象レコードIDを抽出できるレポートを作成できたら後はエクスポートしてデータローダで削除するだけです。
データローダでどのオブジェクトを指定するかはこちらが参考になります。
FeedItemを削除すればいいのですが、間違いが無いか一度にエクスポートなどで検証を行ってください。Chatter投稿を削除することはほぼありませんが、削除後復元は難しいと思いますので念入りに検証してからが良いと思います。
レポートで抽出すると対象の件数を確認できます。なので関係のないレコードが含まれていないかの確認ができますし、万が一過ちがあってもすべてが消えるといったことは回避できるはずです。
長くなりましたがChatterの大量誤投稿を行ってしまった場合はこんな感じで修復が行えます。
より便利なChatter投稿を行うために
最後にChatterの自動投稿でやっておきたい設定を紹介します。Chatterグループに自動で投稿したい場合は次の設定になると思います。
この場合、Chatterフィードから対象のレコードに移動することができません。なので投稿先を「このレコード」に指定して本文の中にグループメンションを追加します。
これでChatterグループにレコードへのリンクがある状態で投稿することが可能になります。
プロセスビルダーでChatter投稿機能を実装するときはこのあたりを押させておくといいと思います。
参考
SFDC:承認割り当てメールテンプレートで申請通知メールのカスタマイズ
Salesforceには承認プロセス機能を利用することで承認申請の仕組みを実装することができます。
申請時には通常次の通知メールが自動で送信されるようになっています。
このメールの本文は承認割り当てメールテンプレートの設定を行うことでカスタマイズできます。検証用に次のメールテンプレートを用意しました。
こんな感じです。
これで承認担当者に届く通知メールをカスタマイズできました。
これだけだと承認申請ページへのリンクも無く不便です。差し込み項目ルックアップを使って更にカスタマイズします。
承認の項目を選択して利用できる項目を差し込めば良いみたいです。
試してみた所、ページのURLは問題なく表示されました。(通常ユーザの場合は社内、社外どちらのURLも同じでした。)
ただ、申請相手の名前部分は表示されませんでしたので差し込んだ項目に誤りがあったようです。「承認ステップの任命先」と「承認ステップの承認者」は申請した人が割り当てられるので、承認者側の名前ではありませんでした。
承認者側の名前を差し込む項目だけ見つけられませんでしたが、ひとまずこれで申請通知メールのカスタマイズはできそうです。メールの本文をSalesforceデフォルトのものから変更したい場合はここから設定が可能でした。
SFDC:Lightning Experienceと取引先階層の管理
Salesforceの取引先には階層の管理機能が用意されています。
この機能をLightning Experienceで利用しようとすると下記エラーメッセージが表示されると思います。
このメッセージが表示されたらオブジェクトマネージャーの取引先設定画面で階層の列を設定すれば解決します。
これでLightning Experienceでも取引先の階層を表示できるようになります。今まで表示項目のカスタマイズはできませんでしたが、LEXでは自由にカスタマイズが可能になるようです。
SFDC:Salesforce Trailhead Live Tokyoの資料ダウンロードサイト
7月7日にSalesforce Trailhead Live Tokyoの資料ダウンロードサイトが公開されました。一部非公開のセッションもありますが、セッションの録画動画を見ることもできます。
SFDC:Lightningコンポーネント開発とForce.com IDE
VisualforceやApexの開発をするとき最近はSublimeText × MaventsMateを利用することが多いのですがTrailheadを見ているとLightningコンポーネントの開発ツールとしてForce.com IDEが紹介されていました。
Google翻訳で訳した内容はこちら。
Force.com IDEはしばらく利用したことがなかったのですが、最新バージョンではLightningコンポーネント開発もサポートされているみたいです。
SFDC:Einstein Data Discovery 組織へのログインを試してみました
Einstein Data Discovery組織へのログインを試してみました。TrailheadのEinstein Data Discovery の基礎で紹介されているトライアル用の組織です。
Einstein Data Discovery とは?
下記の説明がTrailheadで紹介されています。
Einstein Data Discovery は、当初は BeyondCore によって開発されました。Salesforce が同社を買収した後に Analytics Cloud Einstein の一部となりました。
Einstein Data Discovery を使用すると、ビジネスユーザは、高度なソフトウェアや統計モデルを構築しなくても、データに基づいて関連する事実やテーマを自動的に発見できます。それはまるで、自分専任のデータサイエンティストがいて、膨大な量のデータをすばやくより分け、重要な相関関係を発見して正確な予測を作成してくれるようなものです。データの分析が完了すると、Einstein Data Discovery は、ビジネスユーザが理解しやすいように回答、説明、推奨を生成します。
Trailheadリンク
使い方
パスワードの条件は少し厳し目になっています。
サインアップすると次の画面が表示されます。
一度ログインすると次回以降は下記URLのページからログインできるそうです。
動作確認用に投入するテストデータですがTrailheadからCSVファイルをダウンロードできます。
Datasetsのcsvを選択します。
対象のcsvファイルを選択するだけで・・・
簡単にデータを取り込めました。
Reviewボタンをクリックすると詳細ページに移動します。Trailheadに詳しく書いてありますが、エラーがでているようです。
ActionのメニューにあるFind 〜 ボタンをクリックします。
これでエラーになっている値をこの場で修正(置換)できるみたいです。
実行すると次の画面が表示されました。この画面はDatasetsのメニューからアクセスできるようになっています。
右上のCreateボタンをクリックするとストーリーが作成できるみたいです。
いろいろ条件設定できるのですが今回は初期値のままで右上のCreate Storyボタンを押します。
これでStoryが作成できました。
文字とグラフで少し面倒な感じですが、日本語訳すると意味がわかって読む気が出ました。取り込まれたデータを分析してその結果を解説してくれるみたいです。
画面右上のExportボタンをクリックするとPowerPointまたはWord形式でExportできます。
すごかったです。
今回試したモジュールではここまででした。おそらく現時点では英語のみの製品なので利用する機会は無いと思いますがAnalytics Cloud Einsteinで使えるEinstein Data Discoveryはすごそうな製品でした。
SFDC:GitHubでバージョン管理
TrailheadにGitHubをつかったバージョン管理についてのモジュールが追加されています。GitHubで何ができるかや初期セットアップ、基本的なコマンドの意味、プロジェクトのフローなどが解説されていました。
まだ英語版ですがChromeの日本語翻訳機能を使うとざっくりとした意味を書くにできると思います。
関連
SFDC:Commerce Cloudの事例サイト一覧
Commerce Cloudの事例サイト一覧が公開されています。
Commerce Cloudはショッピングサイトを構築できる製品で、多言語化対応やモバイル対応などもサポートするWebサイトを構築できるようです。またEinsteinとの連携で購入情報を分析してオススメの商品を強調したり..的なことができるみたいです。他にも一時的にネットワークに繋がらなくなった時も問題なく利用できるResilient POSという機能があったりするみたいです。
詳細はTrailheadにモジュールが用意されています。
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アプリの場合は一時的にオフラインになった場合でも非同期で保存処理を実行する仕組みがあるそうです。
開発ドキュメント
実際に動くコード
上記の開発者ドキュメントページのサンプルコードに取引先責任者の保存機能がありましたので試してみました。
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』が宣言されているので、クイックアクションで利用できます。
作成したアクションはページレイアウトで追加できます。
動かしてみたらエラーメッセージ。
アレっと思ってコードをちゃんと確認したら取引先のページに設置するためのコンポーネントでした。ということで、取引先アクションとして作り直して・・・
無事に動かすことができました。
保存処理もサクサク動きます。必須項目の値が未入力の場合は入力欄の下にリアルタイムでエラーコメントが表示されたりしました。
シンプルなLightningコンポーネントの開発なら少ない工数で実装を可能にしてくれそうです。
SFDC:UX Prototyping Basics
TrailheadにUX Prototyping Basicsのモジュールが用意されています。このモジュールでは開発前にプロトタイプの実装を行うことのメリットが紹介されていました。またSalesforce社ではどのようにこういった作業をおこなっているかも触れられていました。
Lightning PageとLightning Componentを作りながらプロトタイプ作成→本実装までの流れを確認することができます。次のような画面を開発することができました。
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タグなど開発がやりやすくなっているようでした。
SFDC:Company-Wide Org Settings
TrailheadのCompany-Wide Org Settingsのモジュールでは組織設定 (タイムゾーンやマルチ通貨)やサイドバーカスタマイズ、ユーザインターフェイズ設定や検索結果の設定について確認することができました。
SFDC:Platform Eventsを試してみました
Trailheadを見ながらPlatform Eventsの機能を試してみました。
Platform Eventsの作成
設定で「Platform Events」と検索。New Platform Eventボタンをクリックします。
ラベルや説明項目を入力します。
拡張子は__eとなりました。
検証用に下記項目を作成します。(詳細はTrailheadを確認のこと)
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()); } } }
プロセスビルダーをつかった公開
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から実行してみました。
結果はこちら
ひとまず正常に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; }
ケースを作成するトリガを用意した後に、先程のApexをつかったPlatform EventsのINSERT処理を実行します。するとApexトリガが実行されて無事にケースが登録されました。
使い方はこんな感じで良いみたいです。
Apexテスト
専用のテストの書き方が用意されています。
@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でプラットフォームイベント通知を購読する
この辺は試していないのでTrailhead要確認という感じです。
Platform Eventsの使い方はこんな感じでした。
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:VF上書き設定後に標準ページに切り替える方法
忘れるのでメモ。下記パラメータを付与します。
/e?nooverride=1
SFDC:Salesforce DXの環境構築を試してみました - Part1
Salesforce DXの環境構築を試してみました。TrailheadのApp Development with Salesforce DXで学ぶことができます。
30日間利用できるトライアル環境が必要になります。
ログイン後の画面です。
CLIは下記URLからインストールできます。
Mac用
インストールできているかは下記コマンドで確認できます。
$ sfdx
便利なコマンド
利用可能なすべてのトピック
$ sfdx force --help
使用可能なすべてのコマンド
$ sfdx force:doc:commands:display
Dev Hubにログインする
$ sfdx force:auth:web:login -d -a DevHub
実行するとログインページに移動します。DevHubのトライアル環境にログインします。
ログインすると認証ページに移動します。
これでDev Hub環境にログインできました。
一度ログインすると次のコマンドでDevHub組織にアクセスできます。
sfdx force:org:open -u DevHub
サンドボックスにログインする場合は次のとおりです。
$ sfdx force:auth:web:login -a FullSandbox $ sfdx force:auth:web:login -a DevSandbox
The Power of Aliasing - 別名の扱い
$ sfdx force:org:open -u FullSandbox $ sfdx force:org:open -u MyScratchOrg $ sfdx force:limits:api:display -u DevSandbox
すべての組織を表示 (画面キャプチャは組織なしの場合)
$ sfdx force:org:list
『--verbose』オプションでより詳細を確認できるそうです。
プロジェクトの作成
下記のコマンドでプロジェクトを作成できます。
$ sfdx force:project:create -n geolocation
ファイルのイメージ (Google翻訳版)
スクラッチ組織を作成する
$ sfdx force:org:create -s -f config/project-scratch-def.json -a GeoAppScratch
上のエラーがでたときはディレクトリが正しい確認します。
これで正常に実行できました。
コマンドのオプションの意味はこちら
作成できたかはDevHub組織で確認できます。
カスタムオブジェクトを作成
下記コマンドで作成したスクラッチ組織にアクセスできます。
$ sfdx force:org:open
スクラッチ組織にアクセスしたら取引先にカスタム項目を作成します。
権限セットを作成します。
次のコマンドで権限セットをユーザに割り当てることができます。
$ sfdx force:user:permset:assign -n Geolocation
変更をローカルプロジェクトに取込
Salesforce環境を反映するスクラッチ組織を作成し、そのスクラッチ組織内で直接いくつかの変更を加えました。今、魔法が起こります。1つのコマンドで、アプリケーションをビルドしたときに作成されたすべてのメタデータをプロジェクトに取り込むことができます。
$ sfdx force:source:pull
ベストプラクティスとして、プロジェクトに持ってきたソースをすぐにVCSにコミットします。スクラッチオルガンは一時的で一時的なものなので、ローカルに保存した作業のバックアップが常に必要です。
Salesforce DXはバージョンコントロールに依存しないため、好きなシステムを使用できます。GitHubで使用できるGitコマンドの例を次に示します。レポを初期化してGitHubに接続するには、次の1回限りのコマンドを使用します。
$ git init $ git remote add origin [github ssh url]
これらのコマンドは、ファイルをリポジトリmasterにコミットします。
$ git add -A $ git commit -m “Added custom object and permset” $ git push origin master
どのVCSを使用する場合でも、.sfdxフォルダをリポジトリに追加しないように設定することをお勧めし ます。このフォルダには、スクラッチオーガニックの一時的な情報が格納されているため、VCSに後世向けに保存する必要はありません。gitでは、.gitignoreファイルに追加します。
VCSでは、更新されたオブジェクト定義が安全で健全です。しかし、関連するデータがなければそれほど興味深いものではありません。スクラッチオルガンには、選択したエディションに基づいた標準データが付属しています。ただし、作成するアプリや成果物に関連するサンプルデータを追加することは重要です。この例では、新しいカスタムLocationフィールドでサンプルデータを使用できます。Salesforce CLIを使用して、新しい複合フィールドにいくつか追加しましょう。
サンプルデータの作成
テストデータを作成します。
次のコマンドでクエリを実行して対象データをエクスポートできます。
$ sfdx force:data:tree:export -q "SELECT Name, Location__Latitude__s, Location__Longitude__s FROM Account WHERE Location__Latitude__s != NULL AND Location__Longitude__s != NULL" -d ./data
エクスポートしたファイルはJSON形式でdataフォルダ内に保存されます。
エクスポートした情報は次のコマンドでインポートできます。
$ force:data:tree:import --sobjecttreefiles data/Account.json
これでCLIのインストール、DevHub環境への接続、スクラッチ組織の作成、権限セットの割り当て、サンプルデータのエクスポートとインポートの方法まで確認できました。
補足
sfdx-project.jsonファイルにはどのような情報が含まれていますか?
ソースとメタデータをスクラッチ・オーガナと同期するために必要な情報
MyNewAppというプロジェクトワークスペースを作成するためのCLIコマンドは何ですか?
sfdx force:project:create -n MyNewApp
既存のアカウントをインポートするためのCLIコマンドの例は、異なるプロジェクトワークスペースのデータのサンプルですか?
sfdx force:data:tree:import --sobjecttreefiles data/Account.json