【Salesforce】手動の共有設定を子レコードにも一括で反映させるLWC

## やりたいこと
親レコードの共有の情報を子レコードにも反映させたい！

### 前提条件
・カスタムで「機密情報」と「機密情報詳細」オブジェクトがあるとします
・機密情報詳細は機密情報への参照項目を持っています
・「機密情報」と「機密情報詳細」オブジェクトは基本的に非公開で、詳細画面の共有ボタンから共有されているユーザまたはグループにしか権限がないとします
機密情報の詳細画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/c73f21e0-3bec-bdb5-e8d8-6b512809c66f.png)

設定の共有設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/6e3bfd01-5a90-c5cd-1238-323c3046c1e8.png)

詳細画面から手動で設定する共有
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/4eebeb8e-91a3-4b48-d89a-fdb5298157c6.png)

:::note info
上記の共有設定を子レコードにもボタンひとつで反映させたい
:::

## 実装内容
機密情報オブジェクト（親）に子コードも含めて、共有設定の詳細が確認できるコンポーネントをLWCで実装しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/aad15b30-41df-9930-54b4-70d32c0f088e.png)

「この共有設定を以下のレコードにも反映」ボタンを押下すると、確認画面が開きます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/cbf4563d-4662-d715-5afc-c9012f5470db.png)

「はい」を押下し、ページをリフレッシュすると、子レードにも親と同じ共有設定になっていることがわかります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2640548/48428b10-edaf-ffde-1e3e-f3fa8d6ea40f.png)

### コーディング詳細
reflectShareInfoという名前でLWCを作成しました。JavascriptとHTMLは以下の通りです
```reflectShareInfo.js
import { api, LightningElement, wire, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import secretRelatedInfo from '@salesforce/apex/GetShareInfoController.secretRelatedInfo';
import startReflectShareInfo from '@salesforce/apex/ReflectShareInfoController.startReflectShareInfo';
import MyModal from 'c/reflectShareinfoModal';

const columns = [
    // { label: '機密情報名', fieldName: 'recordName', type: 'text' },
    { label: 'ユーザまたはグループ名', fieldName: 'UserOrGroupName', type: 'text' },
    { label: 'アクセス種別', fieldName: 'RowCause', type: 'text' },
    { label: 'アクセスレベル', fieldName: 'AccessLevel', type: 'text' },
];

export default class ReflectShareInfo extends LightningElement {
    @api recordId;
    @track thisRecordtData;
    @track tableData;
    @track columns = columns;

    @wire(secretRelatedInfo, {siId: '$recordId'})
    gettableData({ error, data }) {
        if (data) {
            // secretInfoDetailAndShareInfoは編集できないため、編集できる形に再定義
            var targetData = JSON.parse(JSON.stringify(data.secretInfoDetailAndShareInfo));
            let tmpTableData = []
            // 関連レコードの共有設定のテーブル情報
            Object.keys(targetData).forEach(function(element){
                for(var i = 0; i<targetData[element].length; i++) {
                    // テーブルデータは参照情報を扱ってくれないため、参照情報を取り出してテーブルデータとして定義
                    var targetDataUserOrGroupName = targetData[element][i]['UserOrGroup']['Name'];
                    targetData[element][i]['UserOrGroupName'] = targetDataUserOrGroupName;
                    var sidRecordInfo = data.siDetailMap[targetData[element][i]['ParentId']];
                    targetData[element][i]['targetRecordName'] = sidRecordInfo['Name'];
                    var recordName = targetData[element][i]['Parent']['Name'];
                    targetData[element][i]['recordName'] = recordName;
                    
                }
                tmpTableData.push({key:element, value:targetData[element]});
            });
            this.tableData = tmpTableData;

            // 表示されているレコードの共有情報整備
            var displayedShareData = JSON.parse(JSON.stringify(data.sishlst));
            displayedShareData.forEach(element => {
                // console.log(element);
                element['UserOrGroupName'] = element['UserOrGroup']['Name'];
            });
            this.thisRecordtData = displayedShareData;
        }else if(error){
            console.log('Something happened: ' + JSON.parse(JSON.stringify(error)));
        }
    }

    // 共有設定を反映するボタンを押下した際に起動
    async handleClick() {
        console.log('Click pushed');
        var result = await MyModal.open({
            size: 'small',
            label: 'sample label',
            description: 'Confirm the share info'
        });
        console.log(result);
        
        if (result) {
            await startReflectShareInfo({siId: this.recordId}).then(result =>{
                console.log('success');
                this.showSuccessToast('成功', '共有設定の反映が成功しました。画面をリフレッシュして共有情報をご確認ください')
            }).catch(error =>{
                console.log('Something happened: ' + JSON.parse(JSON.stringify(error)));
                this.showErrorToast('失敗', '共有設定の反映が失敗しました')
            })
        }
    }

    showErrorToast(title, message) {
        const evt = new ShowToastEvent({
            title: title,
            message: message,
            variant: 'error',
            mode: 'dismissable'
        });
        this.dispatchEvent(evt);
    }

    showSuccessToast(title, message) {
        const evt = new ShowToastEvent({
            title: title,
            message: message,
            variant: 'success',
            mode: 'sticky'
        });
        this.dispatchEvent(evt);
    }
}
```

```reflectShareInfo.html
<template>
    <lightning-card title='このレコードの共有情報' icon-name="custom:custom11" if:true={thisRecordtData}>
        <lightning-datatable
                key-field="id"
                data={thisRecordtData}
                columns={columns}
                hide-checkbox-column
                show-row-number-column>
        </lightning-datatable>
        <div slot="footer" class="slds-clearfix"> 
            <lightning-button
                label="この共有設定を以下のレコードにも反映"
                title="Non-primary action"
                class="slds-var-m-left_x-small slds-float_left"
                onclick={handleClick}>
            </lightning-button>
        </div>
    </lightning-card>
    <template for:each={tableData} for:item="shareData">
        <lightning-card title={shareData.key} icon-name="custom:custom11" if:true={tableData} key={shareData.key}>
            <lightning-datatable
                    key-field="id"
                    data={shareData.value}
                    columns={columns}
                    hide-checkbox-column
                    show-row-number-column>
            </lightning-datatable>
        </lightning-card>
    </template>
</template>

```

モーダル画面はreflectShareinfoModalという名前で作成しています
```reflectShareinfoModal.js
import LightningModal from 'lightning/modal';

export default class ReflectShareinfoModal extends LightningModal {
    handleYes() {
        this.close(1);
    }
    handleNo() {
        this.close(0);
    }
}
```
```reflectShareinfoModal.html
<template>
    <lightning-modal-header label="以下の内容でよろしいですか？"></lightning-modal-header>
    <lightning-modal-body> 
        <p>既存の共有設定を上書きしますがよろしいですか？</p>
    </lightning-modal-body>
    <lightning-modal-footer>
        <lightning-button label="はい" onclick={handleYes} class="slds-m-right_x-small"></lightning-button>
        <lightning-button label="いいえ" onclick={handleNo}></lightning-button>
    </lightning-modal-footer>
</template>
```

現状の共有設定の取得処理
※共有の情報を管理しているオブジェクトは[オブジェクト名]__Shareというオブジェクトです（標準オブジェクトの場合は[オブジェクト名]Share）
詳細は以下を参照ください

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_bulk_sharing_creating_with_apex.htm

```GetShareInfoController.cls
public with sharing class GetShareInfoController {

    public class SecretRelatedInfoReturn {
        @AuraEnabled public List<SecretInfo__Share> sishlst;
        @AuraEnabled public Map<Id, List<SecretInfo__Share>> secretInfoAndShareInfo;
        @AuraEnabled public List<SecretInfoDetail__Share> srdlst;
        @AuraEnabled public Map<Id, SecretInfoDetail__c> siDetailMap;
        @AuraEnabled public Map<String, List<SecretInfoDetail__Share>> secretInfoDetailAndShareInfo;

    }

    @AuraEnabled(cacheable=true)
    public static SecretRelatedInfoReturn secretRelatedInfo(Id siId) {
        System.debug('########start getSecretInfo method');
        System.debug(siId);
        SecretRelatedInfoReturn sri = new SecretRelatedInfoReturn();

        Map<Id, SecretInfo__Share> secretInfoShareMap = new Map<Id, SecretInfo__Share>(
            [
                SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause, UserOrGroup.Name
                FROM SecretInfo__Share
                WHERE ParentId = :siId
            ]
        );
        sri.sishlst = secretInfoShareMap.values();
        Map<Id, List<SecretInfo__Share>> parentIdSecretInfoShare = new Map<Id, List<SecretInfo__Share>>();
        for (SecretInfo__Share variable : secretInfoShareMap.values()) {
            System.debug(variable);
            if (!parentIdSecretInfoShare.keySet().contains(variable.ParentId)) {
                parentIdSecretInfoShare.put(variable.ParentId, new List<SecretInfo__Share>());
            }
            parentIdSecretInfoShare.get(variable.ParentId).add(variable);
        }

        sri.secretInfoAndShareInfo = parentIdSecretInfoShare;

        Map<Id, SecretInfoDetail__c> secretInfoDetailMap = new Map<Id, SecretInfoDetail__c>(
            [
                SELECT Id, Name, SecretInfo__c
                FROM SecretInfoDetail__c
                WHERE SecretInfo__c = :siId
                WITH SECURITY_ENFORCED
            ]
        );

        sri.siDetailMap = secretInfoDetailMap;

        Map<Id, SecretInfoDetail__Share> secretInfoDetailShareMap = new Map<Id, SecretInfoDetail__Share>(
            [
                SELECT Id, Parent.Name, ParentId, UserOrGroupId, AccessLevel, RowCause, UserOrGroup.Name
                FROM SecretInfoDetail__Share
                WHERE ParentId IN :secretInfoDetailMap.keySet()
            ]
        );
        Map<String, List<SecretInfoDetail__Share>> parentIdSecretInfoDetailShare = new Map<String, List<SecretInfoDetail__Share>>();
        String keyElement;
        for (SecretInfoDetail__Share variable : secretInfoDetailShareMap.values()) {
            System.debug(variable);
            keyElement = variable.Parent.Name + '-' + variable.ParentId;
            if (!parentIdSecretInfoDetailShare.keySet().contains(keyElement)) {
                parentIdSecretInfoDetailShare.put(keyElement, new List<SecretInfoDetail__Share>());
            }
            parentIdSecretInfoDetailShare.get(keyElement).add(variable);
        }

        sri.secretInfoDetailAndShareInfo = parentIdSecretInfoDetailShare;
        System.debug('sri: ' + sri);
        sri.srdlst = secretInfoDetailShareMap.values();
        return sri;
    }
}
```
共有設定の反映を担当しているApex。
子レコードに既存の共有設定がある場合は、削除してから親レコードの共有設定の内容を上書きしています。
```ReflectShareInfoController.cls
public without sharing class ReflectShareInfoController {
    @AuraEnabled
    public static Boolean startReflectShareInfo(Id siId) {
        
        // 表示している機密情報レコードの共有情報を取得
        List<SecretInfo__Share> siShareList = [
            SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause
            FROM SecretInfo__Share
            WHERE ParentId = :siId
            AND RowCause = 'Manual'
        ];
        System.debug('siShareList取得後');
        System.debug(siShareList);

        // 表示している機密情報レコードに紐づく機密情報詳細レコードを取得
        List<SecretInfoDetail__c> sIDList = [
            SELECT Id, Name, SecretInfo__c, SecretInfo__r.Name
            FROM SecretInfoDetail__c
            WHERE SecretInfo__c = :siId
        ];
        System.debug('sIDList取得後');
        System.debug(sIDList);

        // 機密情報詳細レコードの共有情報で手動設定されたものを取得
        List<SecretInfoDetail__Share> sidShareList = [
            SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause
            FROM SecretInfoDetail__Share
            WHERE ParentId IN :sIDList
            AND RowCause = 'Manual'
        ];
        System.debug('sidShareList取得後');
        System.debug(sidShareList);

        if (Schema.SObjectType.SecretInfoDetail__Share.isDeletable()) {
            if (sidShareList.size() > 0) {
                System.debug('delete前');
                delete sidShareList;
            }
        }else {
            System.debug('共有設定の削除権限がありません');
            return False;
        }
        // 機密情報詳細レコードの共有情報を取得
        List<SecretInfoDetail__Share> afterDeleteShareList = [
            SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause
            FROM SecretInfoDetail__Share
            WHERE ParentId IN :sIDList
        ];
        // Mapを作成
        Map<Id, SecretInfoDetail__Share> secretDetailShare = new Map<Id, SecretInfoDetail__Share>();

        // UserOrGroupIdをキー、SecretInfoDetail__Shareをバリューにするマップを作成
        for (SecretInfoDetail__Share scretShareInfo : afterDeleteShareList) {
            secretDetailShare.put(scretShareInfo.UserOrGroupId, scretShareInfo);
        }
        System.debug('afterDeleteShareList取得後');
        System.debug(secretDetailShare);

        // 表示している機密情報レコードの共有情報を関連する機密情報詳細レコードへコピー
        List<SecretInfoDetail__Share> newSecretInfoDetailShareList = new List<SecretInfoDetail__Share>();
        for (SecretInfoDetail__c targetSecretInfoDetail : sIDList) {
            for (SecretInfo__Share siShareInfo : siShareList) {
                // 機密情報詳細レコードの所有者や設定の共有でデフォルトで共有されているユーザやグループを考慮
                if (secretDetailShare.containsKey(siShareInfo.UserOrGroupId)) {
                    System.debug('共有済みのためスキップ');
                    continue;
                }
                // 新規の共有設定を作成
                SecretInfoDetail__Share sidShare = new SecretInfoDetail__Share();
                sidShare.ParentId = targetSecretInfoDetail.Id;
                sidShare.UserOrGroupId = siShareInfo.UserOrGroupId;
                sidShare.AccessLevel = siShareInfo.AccessLevel;
                newSecretInfoDetailShareList.add(sidShare);
            }
        }
        if (newSecretInfoDetailShareList.size() > 0) {
            System.debug(newSecretInfoDetailShareList);
            insert newSecretInfoDetailShareList;
        }

        return True;
    }
}
```

## 終わりに
ある特定の部署などによっては、レコードの情報をオープンにしたくないという要望があるかと思います。
そんな時のための参考になれば幸いです:grinning: