Salesforce開発関連メモ

# 初めに
Salesforce開発に関して、実際に実装が必要になった際に調べて役に立ったTipsを延々と紹介します（随時更新）

## Apex関連
### 文字列配列の初期化&文字追加
- Java

```java
String[] str = {“a”, “b”, “c”};
List<String> strList = new List<String>(Arrays.asList("Apple", "Orange", "Melon"));
```

- Apex  
```apex
List<String> strList = new List<String>{'a', 'b', 'c'};
```

### null、空文字チェック
https://qiita.com/s_hayashida/items/15e2fcadd2140702aa96#list-set-map

### オブジェクト項目の数値型をIntegerに変換
- そのまま取得した数値型を `obj__c.ItemNumber__c == 1`とか比較したりはできない  

```apex
System.assertEquals(Integer.valueOf(Account.Number_of_Contacts__c) ,1);
```

### オブジェクトから取得したデータを元にループ

```apex
for (Account account : [select id from account]) {
    contacts.add(new Contact(firstname='first', lastname='last', accountId=account.id));
}
```

### List型変数内の同じ値を数える
- Mapを使う
- String型にしているが、型を変えれば汎用的に使用OK（標準関数ない？）

```apex
public static Map<String, Integer> findDuplicateCount(List<String> targetStrList) {
    Map<String, Integer> cntMap = new Map<String, Integer>();

    for(String tmpKey : targetStrList) {
        if(!cntMap.containsKey(tmpKey)) {
            cntMap.put(tmpKey, 0);
        } else {
            cntMap.put(tmpKey, cntMap.get(tmpKey)+1);
        }
    }
    return cntMap;
}
```

### Map型の指定したKey値からValue値を取得する
- `KeySet()`を使用するとSet型のリストが返ってくる
- Setをループすればいいだけだが、1Linerでやりたいとき（混乱するので丁寧な解説を）
    - `KeySet()`で取得するSetをList型に変換
    - Key値のList型変数になる
    - Index値を指定してKey値を取り出す
    - 指定のMapに対して `get()`でKeyを指定して値を取り出す
- 例はindex0の値を取り出す例

```apex
System.debug('Current Key: ' + new List<String>(tmpMap.keySet()).get(0));
System.debug('Current Value By Key: ' + tmpMap.get(new List<String>(tmpMap.keySet()).get(0)));
```

### Cloneメソッド
- いきなり出てきて焦った。しかも`DeepClone`もあるらしい。  
  - `Clone`は単純に現在のデータをCloneして、新しいIdで作り直す。
  - `DeepClone`は関連リストも含めてCloneするらしい。  
  - [参照: Clone and DeepClone](https://developer.salesforce.com/forums/?id=906F0000000kCX9IAM)

```apex
clone(preserveId, isDeepClone, preserveReadonlyTimestamps, preserveAutonumber))
```

### レコードの一括作成
`sObject`型でデータを作ってListにaddしてinsertするだけ.

```apex
List<Contact> lstContact = new list<Contact>();

for(Account acc: lstAccounts)
{
    Contact objContact = new Contact(LastName = 'test', AccountId = acc.Id);
    lstContact.add(objContact );
}

if(!lstContact.isEmpty())
{
   insert lstContact ;
}
```

### 作成したレコードの`Id`の取得
- フローとかでできるやつ、Apexでどうやる？ってなったが、Insertした後のレコード変数の`Id`にアクセスするだけだった。

```apex
Account acc = new Account(Name='Test');
insert acc;

system.debug("Inserted Account Id = " + acc.Id);
```


### レコードの更新
- リストの中のフィールドを更新するとき

```apex
List<Account> acc =[SELECT Id, Name FROM Account];
List<Account> newAcc = new List<Account>();
for(Account a : acc){
    a.Name ='Test';
    newAcc.add(a);
}

update newAcc; //upsertでも可
```

### 複数レコードの更新をする際に、成功したものはコミットさせたい場合
`Database.SaveResult[]`を使用して`allOrNone`パラメータを`false`にしておく    
普通に`insert`を使用してしまうと一つでもエラーになった場合全てロールバックされるため  

```apex
// Add Account
Account acct = new Account(
    Name='Test Account',
    NumberOfEmployees=10,
    BillingCity='Tokyo');
List<Database.SaveResult> results = Database.insert(acct, false);
```
ちなみに、upsertの場合は下記となります
```apex
List<Database.UpsertResult> results = Database.upsert(ops, false);
```

メソッドと返り値はこちらが詳しい:  
https://qiita.com/t_yano/items/19f8462f41b2a2bafded

### レコードの復元
- `RECORD_ID`を復元したい再生レコードIdに変更すること。
- 再生レコードIdは変更データキャプチャしてれば取得できるが、それ以外の方法はまだ調べていない

```apex
Employee__c record = [SELECT Id,Name FROM Employee__c WHERE Id='RECORD_ID' ALL ROWS];
undelete record;
```

### 乱数を作りたい場合
- 下記のメソッドで作成。
- 引数として `UPPER_LIMIT`を渡しても良いし、Constにしても良いかと。
- 帰ってきた値で配列のindexを指定すれば文字列やレコードなんかもいけます。
```apex
static final Integer UPPER_LIMIT = 3;
Integer getRandomNumber() {
    Integer rand = Math.round(Math.random()*1000);
    return Math.mod(rand, UPPER_LIMIT);
}
```
こちらのGitHubのソースを参考に:
[GitHub: Get Random Number](https://github.com/alexed1/LightningFlowComponents/tree/master/flow_action_components/Summer18/GetRandomNumber)
あるいはこちら:
https://qiita.com/Supply-net/items/c3e63edd2a3a260f06ba

### 正規表現で日本語を見つける
`.find()`はBooleanが返却される。

```apex
String regex = '[\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}]';
Pattern regexPattern = Pattern.compile(regex);
Matcher regexMatcher = regexPattern.matcher(tmpStr);
if(regexMatcher.find()) {
    // ...
}
```
### ユーザのタイムゾーンに合わせた時間を取得

```apex
TimeZone tz = UserInfo.getTimeZone();
DateTime dt = Datetime.now();
// offset分の秒をGMTタイムゾーンに足す
DateTime nowJST = dt.addSeconds(tz.getOffset(dt)/1000);
```

### Datetime値からDate値を取得して計算
- 例は1年足している

```apex
obj2.Date__c = obj1.CreatedDate.date().addYears(1);
```

### Datetimeの差分を計算
- 秒(/1000だけ)
- 分(/1000/60)
- 時間(/1000/60/60)
- 日(/1000/60/60/24)などは計算で取得  

```apex
Decimal days = decimal.valueOf(nowJST.getTime() - tmpSA.SchedStartTime.getTime())/1000/60/60/24;
```


### ある時点のデータベース値を取得して、エラーが発生した場合はRollBackしたい場合
- 下記の通りに実装。

```apex
Savepoint sp = Database.setSavePoint();

try {
    // logic
} catch(Exception ex) {
    Database.rollback(sp);
    return ex.getMessage();
}
```

### あるオブジェクトの全ての項目API名を取得して、動的にSOQLを発行したい場合
- `SELECT * ...`ができないので項目名をいちいち記載したくないとき  
- `FieldDefinition`オブジェクトからAPI名を取得  
- `SObject`型なので`String`に変換
- SOQLクエリを`String`で作成しておき、`Database.query()`を実行  
- `SELECT`のカラム名の','挿入は`String.join()`を使用する  
    - 参考: https://qiita.com/ymt0608/items/0325e8abf35488d29a6f

```apex
    public static List<Boat__c> getData(String dataId) {
        List<FieldDefinition> fieldApiNames = [SELECT QualifiedApiName FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Data__c' ];
        List<String> fieldApiStrNames = new List<String>();
        for(FieldDefinition fieldApiName : fieldApiNames) {
            fieldApiStrNames.add(String.valueOf(fieldApiName.QualifiedApiName));
        }        
        String soql = 'SELECT ' + String.join(fieldApiStrNames, ',') + ' FROM Data__c';
        if(String.isNotBlank(dataId)) {
            soql += ' WHERE dataId__c = :dataId';
        }
        return Database.query(soql);
    }
```

### GEOLOCATION型のレコードの取得
- JSON型で返却したい、LatitudeとLongitudeがメソッドに渡される
- SELECT時も`Geolocation__latitude__s`って感じにしないといけない
- WHEREやORDER BYで`DISTANCE()`、`GEOLOCATION()`を使用しないといけないらしい
    - [Location-Based SOQL Queries](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_geolocate.htm)

```apex
    public static string getDataByLocation(Decimal latitude, Decimal longitude, Id dataId){
        String query = 
            'SELECT Id, Name, Geolocation__latitude__s, Geolocation__longitude__s' +
            'FROM Data__c' +
            'WHERE DataId__c = :dataId' + 
            'ORDER BY DISTANCE(Geolocation__c, GEOLOCATION(latitude, longitude), \'mi\')' +
            'LIMIT 10';
        return JSON.serialize(Database.query(query));
    }
```

### 組織の社内ユーザのみを取得したい場合
- ちなみに、他 `UserType`はこちらを[参考](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_user.htm#profilelicensetype)
- GuestやCommunity Userなども取得可能

```SQL
SELECT Id, Name, UserType FROM User WHERE UserType = 'Standard'
 ```

### 取得したSObject型リストをId型リストに変換する
- `Map`クラスのコンストラクターに便利なものが存在していた...  
    - [Developer Guide 参考](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_map.htm#apex_System_Map_ctor)

```apex
List<Account> acc = [SELECT Id, Name FROM Account];
Set<Id> accIds = (new Map<Id, Account>(acc)).KeySet();
```

### SOQLで取得した結果をMapに変換する
- `Id`と`SObject`型のMapに変換する  

```apex
Map<Id, Product2> prds = new Map<Id, Product2>([SELECT Id, Name FROM Product2 WHERE Id IN :ids]);
```

### 組織のドメイン取得
- 下記でString型で使用可能
- 取得できるのは `https://xxxx.salesforce.com`まで

```apex
System.URL.getSalesforceBaseURL().toExternalForm();

// 例えばケースレコードのLinkなら
String caseLink = URL.getSalesforceBaseUrl().toExternalForm() + '/lightning/r/Case/' + targetCase.Id + '/view/';
```

### メール送信時にファイルを添付する
- あるレコードに添付されているファイルを添付してメールを送信したい場合

```apex
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
// ここはメール送信の宛先を指定する                    
email.setToAddresses(new String[] { asset.ContactsEmail__c });
email.setSubject('テスト件名');
email.setHtmlBody('<html><body>' + 'テスト' + '</body></html>');
// Contact, Lead, UserのIdを指定. ここをセットするとメール追跡が可能
email.setTargetObjectId(asset.ContactId);
                    
// 例:カスタムオブジェクトに添付されたファイルのContentDocumentLinkを取得
List<ContentDocumentLink> contentDocLinks = [
    SELECT ContentDocumentId 
    FROM ContentDocumentLink 
    WHERE LinkedEntityId = :req.customObjectId
];

if (!contentDocLinks.isEmpty()) {
    // ContentDocumentIdを取得
    Set<Id> contentDocumentIds = new Set<Id>();
    for (ContentDocumentLink cdl : contentDocLinks) {
        contentDocumentIds.add(cdl.ContentDocumentId);
    }

    // 最新のContentVersionを取得
    List<ContentVersion> contentVersions = [
        SELECT VersionData, Title, FileExtension 
        FROM ContentVersion 
        WHERE ContentDocumentId IN :contentDocumentIds AND IsLatest = true
    ];

    // ContentVersionの情報をEmailFileAttachmentにadd
    List<Messaging.EmailFileAttachment> attachments = new List<Messaging.EmailFileAttachment>();
    for (ContentVersion cv : contentVersions) {
        Messaging.EmailFileAttachment attachment = new Messaging.EmailFileAttachment();
        attachment.setFileName(cv.Title + '.' + cv.FileExtension);
        attachment.setBody(cv.VersionData);
        attachments.add(attachment);
    }

    email.setFileAttachments(attachments);
}

// メール送信
Messaging.SendEmailResult[] sendResults = Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
                    
if (sendResults[0].isSuccess()) {
    res.isSuccess = true;
} else {
    res.isSuccess = false;
    res.errorMessage = sendResults[0].getErrors()[0].getMessage();
    break; // エラーが発生した場合、ループを中断
}
```

- 上記コード例は1アドレスずつ送信しているが、下記のように`SingleEmailMessage`のリストを作ってまとめて送信することも可能

```apex
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
// 連絡先のリストを取得
List<Contact> contacts = [SELECT Id, Email FROM Contact WHERE Id IN :contactIds];
        
for (Contact c : contacts) {
	Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
	email.setToAddresses(new String[] { c.Email });
	email.setSubject('テストメール - 追跡付き');
	email.setHtmlBody('<html><body>これは追跡付きのテストメールです。</body></html>');

	email.setTargetObjectId(c.Id);
	emails.add(email);
}
// メールを送信
if (!emails.isEmpty()) {
	try {
		Messaging.sendEmail(emails);
		System.debug('メールが正常に送信されました');
	} catch (Exception e) {
        System.debug('メール送信中にエラーが発生しました: ' + e.getMessage());
	}
}
```

## Apex Test
### HTTP Call Outのテストがしたい場合
- Test Classに `HttpCalloutMock`を継承する  
- NGの場合も別途作成する

```apex
@isTest
global class HttpCallOutMockTestMethod implements HttpCalloutMock{
    global HttpResponse respond(HTTPRequest request) {
        HttpResponse response = new HttpResponse();
        response.setHeader('Content-Type', 'application/json');
        response.setStatus('OK');
        response.setStatusCode(201);
        return response;
    }
}
```

- 下記の通り、TestクラスのMethodを使用して`Assert`をかける
- 大概のTestロジックの書き方も下記に習うが、HttpCallOutの場合は`Test.startTest()`などが必要

```apex
@isTest
private class TestClassSample {
    @isTestSetup
    static void MakeData() {
        // テストデータの作成
        Opportunity opp = new Opportunity(Name='TestOK' ...);
        // Custom Settingなど、Calloutで認証情報などがある場合はここで取得
        // some logic here...
    }

    @isTest
    static void HttpCallOutSucessTest() {
        Opportunity opp = [SELECT Id, Name FROM Opportunity WHERE Name = 'TestOK'];
        List<Id> OppIds = new List<Id>();
        OppIds.add(opp.Id);

        // HttpCallOutでの結果を返すMockをセット
        Test.setMock(HttpCalloutMock.class, new ProjectCalloutServiceMock());
        Test.startTest();
           // 下記にテストしたいクラスのメソッドを記述
           testTargetClass.TestTargetMethod(oppIds);
        Test.stopTest();
    
        opp = [SELECT StageName FROM Opportunity WHERE Id =: opp.Id];
        // 期待する結果
        System.assertEquals('XXXXX', opp.StageName);
    }
}
```

## SOQL
### 親から子オブジェクトの項目を取得するSOQL
- 絶対忘れる。 子オブジェクトのquery分は`()`で囲んでおく。  

```apex
List<Account> lstAccount = [SELECT Id, Number_of_Contacts__c, (SELECT Id FROM contacts) FROM account WHERE Id in :setId ];
```

### 子から親オブジェクトの項目を取得するSOQL
- これもいつも忘れる。
```
SELECT FirstName, LastName, Account.Name FROM Contact
```

### キューIDの取得
- これも忘れるんだよなぁ, Groupオブジェクトに対して `Type`を`Queueに指定。  

```apex
Group queue = [SELECT Id FROM Group WHERE Name='Regional Dispatch' AND Type='Queue'];
```

### キューに含まれるユーザIDの取得
- 上記で取得したキューIDを使用
- `Map<Id, GroupMember>`を使用して取得できる `Id`は実際のユーザIDにならないので注意. 

```apex
List<GroupMember> userIdsInQueue = [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :queueId];
```


### 集計関数
- 集計関数を使った際は返却されるデータ型が`AggregateResults`のリスト型になる（なにそれ）  
- apexで結果を取得する際の例は下記  

```apex
List<AggregateResult> results  = [SELECT Industry, count(Id) total
    FROM Account GROUP BY Industry];
for (AggregateResult ar : results) {
    System.debug('Industry: ' + ar.get('Industry'));
    System.debug('Total Accounts: ' + ar.get('total'));
}
```

### ユーザに対してレコードアクセスがあるか確認

```SQL
SELECT RecordId, HasEditAccess FROM UserRecordAccess WHERE UserId = [single ID] AND RecordId = [single ID]
```
### Salesforce組織のオブジェクト名、フィールド名のAPI名を取得するSOQLクエリ
https://qiita.com/mkzyk/items/41328e9cae0ed4a8a4d1

### あるオブジェクトのアクセス権をプロファイルを指定して取得する

```SQL
SELECT Id, Field, SObjectType, PermissionsRead, PermissionsEdit 
FROM FieldPermissions 
WHERE parentId IN ( SELECT id 
                    FROM permissionset 
                    WHERE PermissionSet.Profile.Name = 'システム管理者') AND SObjectType = 'Case'
```

## Flow
### Apex Classを呼び出す際

- `@InvocableMethod`をメソッド前に記載する
- **引数は1つしか渡せません** (Innerクラスを作らない限りは)

```apex
public with sharing class GetRecordsByMultipleIds {
    @InvocableMethod
    public static List<List<Product2>> GetRecordsByMultipleIds(List<List<ID>> inputCollectionIds) {
//...
}
```

### 入力をCollection変数、出力も複数レコードになる場合
- 非常に面倒な仕様です。
    - `List<List<XXX>> var`を引数とします。
    - Flowから渡したリストは `var.get(0)`で取得します。
    - Returnも `List<List<XXX>> retVal`という風にしないといけないです。

```apex
public with sharing class GetRecordsByMultipleIds {
    @InvocableMethod
    public static List<List<Product2>> GetRecordsByMultipleIds(List<List<ID>> inputCollectionIds) {
        List<ID> queryIds = inputCollectionIds.get(0);
        List<Product2> products = [SELECT Id, Name, Family FROM Product2 WHERE Id IN:queryIds ORDER BY Name ASC];
        List<List<Product2>> result = new List<List<Product2>>();
        result.add(products);

        return result;
    }
}
```

### Genericな引数と戻り値にしたい場合
FlowからApexクラスを呼び出す際、結構文字列操作したい場合ってないですか？
いちいちObjectの型を指定せずに、様々なオブジェクトで使いたい場合はよくやりそう。
例えば`Name`のフィールド値とか。
- ちなみにこの場合、Flow側から入力/出力のオブジェクト型を指定することができます！

```apex
public with sharing class TestClass {
    @InvocableMethod
    public static List<List<SObject>> TestMethod(List<List<SObject>> records) {
        List<SObject> tmp = records.get(0);
        for(Integer i=0; i<tmp.size(); i++) {
            // SObjectのNameフィールドの値を取得する
            String name = String.valueOf(tmp.get(i).get('Name'));
            if(expression) {
                //...
            }
        }
        return result; //これはList<List<SObject>>のリスト
    }
{
```

### フローからApexに複数の引数を渡したい場合
- 引数、返却値を内部クラスに指定することで可能だった...!
- Class内に複数変数を定義できるので、かなり自由度が上がる
- 引数はClassのリスト型にする

```apex
public with sharing class IncrementQuantiyForDuplicatedSerialNo {
    @InvocableMethod
    public static List<FlowOutputs> updateQuantityForDuplicatedSerialNo(List<FlowInputs> inputs) {
        FlowInputs input = inputs.get(0);
        List<ProductItem> pi = new List<ProductItem>(input.recordList);
        String scannedSerialNo = input.serialNo;

        List<FlowOutputs> results = new List<FlowOutputs>();
        FlowOutputs rst = new FlowOutputs();

        for(ProductItem tmpPi : pi) {
            if(tmpPi.SerialNumber == scannedSerialNo) {
                tmpPi.QuantityOnHand += 1;
                rst.productName = tmpPi.ProductName;
                rst.serialNo = tmpPi.SerialNumber;
                rst.quantity = tmpPi.QuantityOnHand;
            }
        }

        rst.updatedList = new List<ProductItem>(pi);
        
        results.add(rst);
        System.debug('Return Value: Result ' + results);

        return results;
    }
    
    public class FlowInputs {
        @InvocableVariable
        public List<ProductItem> recordList;

        @InvocableVariable
        public String serialNo;
    }

    public class FlowOutputs {
        @InvocableVariable
        public List<ProductItem> updatedList;

        @InvocableVariable
        public String productName;

        @InvocableVariable
        public String serialNo;

        @InvocableVariable
        public Decimal quantity;
    }
}
```

### 画面フロー上にCSSを読み込みたい場合
- 定数: テキストとして、CSSを入力する

```css:変数名: sampleCss
background-color: #0076D2; border-radius: 30px; color: #fff; padding: 15px 60px; font-size: 18px;
```

- テキストテンプレートを作成する
    - この際に、プレーンテキストを選択
```html:変数名: button
<a href="" style="{!sampleCss}">応募する！</a>
```

下記みたいなのは作れる. (`border-radius` が効かない...)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484310/286712ed-6cec-b380-c70e-7d629e204d65.png)


## Trigger
### 文法
- `Trigger.New`でループさせる。
- Fireの条件が複数ある場合、Triggerのタイミングをキーに処理分岐も可能

```apex
// オブジェクト名指定、いつFireするかのタイミングを引数に指定
trigger TestTrigger on Opportunity (after insert, after update) {
    for(Opportunity opp : Trigger.New) {
        if(trigger.isInsert) {
            // ...
        }
        // ...
    }
}

```

### トリガされた時点での変更前の値の取得
- `after update`の場合に使用するケースが多い

```apex
trigger TestTrigger on Case (after update) {
    for(Case record : Trigger.New) {
        Id priorId = Trigger.oldMap.get(record.Id).OwnerId;
        // logic
        if(priorId != record.OwnerId) {
            // ...
        }
    }
}
```


## LWC
### Lightning Buttonのwidthを100%にしたい際
`slds-button_stretch`をclassにapplyしても適用されない。  
代わりに`style`でwidthを100%にする。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484310/cae848ec-157c-c89c-f8cc-21447fd0f070.png)


```html
<lightning-button 
    label="検索"
    onclick={handleSearchKeyword}
    variant="brand"
    style="display: grid; width: 100%;">
</lightning-button>
```

### LWCから画面フローを呼び出したい場合
- Winter'23からついにLWC版の画面フロー呼び出しが可能に。  
- ハマってしまったのだが、html側からフローを呼び出す際、例えば入力変数がセットされていない場合はフラグなどでFlowを描画するかどうかのフラグを持ち、(`<template if:true={renderFlow}>`などで)Trueになって初めてフローを描画する形にした方が良い。
  - フローを描画するタイミングで確実に入力変数が渡されないと、変数のデータ型タイプエラーが発生することがある。
  - 例: `onclick`でボタンを押した際にデータ精査を行い、`flowInputVariables`に値をセット、その後でフローを描画するなど

- htmlに下記を記述（変数名は固定）  

```html:lwc.html
<template if:true={renderFlow}>
  <lightning-flow
      flow-api-name={flowApiName}
      flow-input-variables={flowInputVariables}
      onstatuschange={handleFlowStatusChange}>
  </lightning-flow>
</template>
```

```javascript:lwc.js
//...
flowApiName = 'flow_name';
flowInputVariables = [];
renderFlow = false;
someFunc() {
    // ...
    this.flowInputVariables = [
        {
            name: 'recordId',
            type: 'String',
            value: this.selectedResourceId
        },
        {
            name: 'InvitationURLPrefix',
            type: 'String',
            value: this.InvitationURLPrefix
        },
    ];
    // 何かしらのロジックで描画を可にするフラグを用意
    this.renderFlow = true;
}

handleFlowStatusChange(event) {
	console.log("flow status : ", event.detail.status);
	if (event.detail.status === "FINISHED") {
		this.dispatchEvent(
			new ShowToastEvent({
				title: "成功",
				message: "サンプルメッセージ",
				variant: "success",
			})
		);
	}
}
```

### 画面フローからLWCを呼び出す際に、Collection変数を渡したい場合
- `js-meta.xml`ファイルに対して、下記の通り記述する
- `type="String[]"`を指定するとエラーがVSCode上では表示されるのだが、なぜかデプロイは成功する  

```xml:sample.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>56.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="sampleNumbers" type="String[]" label="Sample Numbers"/>
            <property name="sampleTexts" type="String[]" label="Sample Texts"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

- JavaScript側は xmlで指定した変数を `@api`化しておくだけ

```javascript:sample.js
import { LightningElement, api } from 'lwc';

export default class DataTableOnFlow extends LightningElement {
    @api sampleNumbers;
    @api sampleTexts;
}
```
- 画面フローに作成したLWCを配置する
- LWCから、コレクション変数が選択できるようになっている

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/484310/1299b34e-9c25-91e7-6d4c-075f15db9a76.png)

### LWCから画面フローに対して値をアウトプットさせたい場合
- 例えば、画面フロー上のLWCが読み込まれた場合に`@wire` apex あるいは 明示的Apexにてデータを取得してアウトプットとして画面フローに戻したい場合
- 一例として、SObjectのリストを返す場合を想定
- `type`に`"@salesforce/schema/SObjectName[]"`を指定する
- ちなみに、Genericな型にInputであればできる？ [参考](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_flow_custom_property_editor_sobject_lwc_example)
Outputは試したらエラーになりました。

```xml:sample.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>56.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="returnData" type="@salesforce/schema/SObjectName[]" label="Return Data" role="outputOnly" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

- Apex側は、フローに返すのであれば、 `List<List<SObject>>`型にしないとイケさそうに思えるが、そもそも `@InvocableMethod` ではないので、xmlと同じで、返却の型は `<List<SObject>`とする。

- JavaScript側は、Outputする変数は `@api`にしておく
- `@wire`のapexメソッドで返却される値に対して、`@api`変数を指定して、問題なく動作しました。

```javascript:sample.js
import { LightningElement, api, wire } from 'lwc';
import testMethod from '@salesforce/apex/TestClass.testMethod';

export default class DataTableOnFlow extends LightningElement {

    @api returnData;

    @wire(testMethod, {strings: '$sampleTexts', nums: '$sampleNumbers'})
    wiredMethod({error, data}) {
        if(data) {
            this.returnData = data;
            this.error = undefined;
        } else if(error) {
            console.log('error')
            console.log(error)
        }
    }
}
```

[参考記事](https://sfactsabhishek.blogspot.com/2021/09/salesforce-fact-180-pass-list-of.html)

### LWC上で音を出すアクションを作成
- 例えばボタンが押されたら、音を出すなど
- まずは、音声ファイルを静的リソースに登録する
- Javacriptで下記の通り実装するだけ

```javascript
// sound from static resource
import staticSuccessSound from '@salesforce/resourceUrl/SuccessSound';

export default class Sample extends LightningElement {
    successSound = new Audio(staticSuccessSound);

    connectedCallback() {
        this.successSound.play();
    }
}
```

### データテーブルに検索機能を付ける
- フローではできるのに、実はLWCでは標準オプションが無いので、作らないといけない

```html
<template if:true={showProductTable}>
    <div class="slds-m-top_large">
        <h3 class="slds-text-heading_small slds-m-bottom_small">検索結果</h3>
        <!-- この部分が検索カラム部分 -->
        <div class="slds-form-element slds-m-bottom_medium">
            <div class="slds-form-element__control slds-grid">
                <div class="slds-col slds-size_4-of-5">
                    <lightning-input
                        id="search-input"
                        type="search"
                        variant="label-hidden"
                        placeholder="検索語を入力"
                        value={searchTerm}
                        onchange={handleSearchTermChange}
                    ></lightning-input>
                </div>
                <div class="slds-col slds-size_1-of-5 slds-p-left_xx-small">
                    <lightning-button
                        label="検索"
                        variant="brand"
                        onclick={handleSearch}
                    ></lightning-button>
                </div>
            </div>
        </div>
        <!-- ここまでが検索カラム部分 -->
        <lightning-datatable
            key-field="Id"
            data={data}
            columns={columns}
            onsave={handleSave}
            draft-values={draftValues}
            hide-checkbox-column
        ></lightning-datatable>
    </div>
</template>

<template if:true={noProductsFound}>
    <div class="slds-text-color_error slds-p-vertical_medium">
        一致する商品がありません
    </div>
</template>
```

```javascript
export default class TestDatatable extends LightningElement {
// ...
// 省略
// ...
    @track searchResult = [];
    searchTerm = '';
// ...
// 省略
// ...   
    handleSearchTermChange(event) {
        this.searchTerm = event.target.value;
    }

    handleSearch() {
        if (this.searchTerm) {
            // apexなどで取得したレコードリストを、`this.searchTerm`を使用して検索
        } else {
            // apexなどで取得したレコードリストを返却
        }

        this.showProductTable = this.searchResult.length > 0;
        this.noProductsFound = this.searchResult.length === 0;
    }
}
```