1. はじめに
この記事に辿り着いた方はご存知と思いますが、Experience Cloud(旧Community Cloud)においてゲストユーザはデフォルトではファイルをアップロードできません。
また、フローだけを使ってコーディング無しで要件を満たすことも2022/3時点では実現出来ません。
詳細理由とフローをベースにしたゲストユーザのファイルアップロード&レコードとの紐付け方法は、コチラをご確認下さい。
この記事では、Lightning Aura ComponentとApexのみでゲストユーザによるファイルのアップロードと新規作成したレコードとの紐付けを1トランザクションで実行する方法を範囲とします。
レコード作成フォームをフルカスタマイズしたい場合を仮定し、フローを使わない方式でやってみます。
2. サイトの公開アクセス
作成したサイトは、不特定多数の人へ公開する機能がExperience Cloudにはあります。
デフォルトでは非公開になっていますが、以下のように設定することで公開されます。
公開状態になるとゲストユーザ用のURLが表示されます。
公開サイトのことを、この記事ではゲストユーザがアクセス可能な状態と表現します。
エクスペリエンスビルダー > 歯車(設定) > 全般タブ > サイトの詳細セクション
3. ゲストユーザへのファイルアップロード許可
ゲストユーザには、デフォルトではファイルのアップロード行為が許可されておらず、明示的に許可する設定を行う必要があります。
以下設定項目の吹き出しを確認すると、「lightning:fileUpload」なるコンポーネントを使用するとアップロードが出来るとあります。
エクスペリエンスビルダーで提供されている標準のコンポーネントでは、ライセンスを持ったユーザ向けにのみアップロード機能があります。しかし、ゲストユーザ向けのサンプルコンポーネントにはアップロード機能はありません!
つまり、「lightning:fileUpload」を使って開発をしなければならないということです。
"lightning:"とある様にLightning Aura Componentで開発しないと、ゲストユーザファイルアップロードが出来ません。
設定 > 機能設定 > Salesforce Files > 一般設定
4. lightning:fileUploadコンポーネント利用
先ほどヒントがあった「lightning:fileUpload」をGoogleで検索すると、Aura Componentのサンプルコードが出てきます。
LWCでは「lightning-file-upload」コンポーネントが同機能を提供するようですが、本記事の対象からは割愛します。
サンプルコードに従いauraを採用します。
読み進めると、Guest_Record_fileupload__cに紐付けたいレコードをセットすると、アップロードと同時に紐付けが行われるとあります。
レコード編集時ならともかく、新規レコード作成時にはIDはまだ発番されていません。
新規レコードに関連付ける為には、予めアップロードしたファイルID情報が必要であると考えることが出来ます。
アップロードしたファイルIDを内部で保持しておき、レコードが作成された後で紐付けを行う方法にします。
以上より公式ドキュメントに反し、ContentVersionオブジェクトにGuest_Record_fileuploadというカスタム項目は作らずに進めます。
<aura:component implements="flexipage:availableForRecordHome,force:hasRecordId">
<aura:attribute name="filetype" type="List" default="['.png', '.jpg', '.jpeg']" />
<aura:attribute name="multiple" type="Boolean" default="true" />
<aura:attribute name="disabled" type="Boolean" default="true" />
<aura:attribute name="recordId" type="String" />
<aura:attribute name="encryptedToken" type="String" />
<lightning:fileUpload label="Attach receipt"
name="fileUploader"
multiple="true"
accept="{!v.filetype}"
recordId="{!v.recordId}"
fileFieldName="Guest_Record_fileupload__c"
fileFieldValue="{!v.encryptedToken}"
onuploadfinished="{!c.handleUploadFinished}" />
</aura:component>
5. ファイル関連オブジェクトリレーション確認
紐付け処理を検討するにあたり、ファイルオブジェクトのリレーションを公式ドキュメントから確認します。
必要な部分を、必要なアトリビュートに絞って抜き出すと、以下のER図になります。
ファイルをアップロードすると、最新バージョンを管理するContentDocumentと、ファイルの実体であるContentVersionが作成されます。
そして、ContentDocumentLinkオブジェクトを連関オブジェクトとして、ファイルとレコードを関連付ける仕組みであると読み取れます。
LinkedEntityIdには、ケースだけではなく任意のレコードのIDを指定出来る様になっています。
ファイルをアップロードしたタイミングで取得できる(筈の)DocumentIdと、ケースを作成した際に得られるCase Idが分かれば紐付けは出来そうです。
新規作成レコードにファイルを紐付ける順序(想定)
①ファイルアップロード時にDocumentIdを取得する
②ケースを登録し、CaseIdを取得する
③ケース登録直後に、DocumentIdとCaseIdを紐づけるContentDocumentLinkレコードを作成する
6. lightning:fileUploadを使ったアップロード実験
以下javascriptで、アップロードしたファイルのContentDocumentIDを取得できる事を確認してみます。①の実証実験です。
({
//アップロード後処理
handleUploadFinished: function (component, event, helper) {
var uploadedFiles = event.getParam("files");
uploadedFiles.forEach(file => {
console.log("contentVersionId:"+file.contentVersionId);
console.log("documentId:"+file.documentId);
console.log("name:"+ file.name);
});
}
})
ファイルをアップロードして、ブラウザのコンソールを確認してみると、、、
あれ、undefined? ..documentId取れてない..?
どうやら、ただではDocumentIdは取れない様です。
ER図に立ち戻って、作戦を練り直します。
ContentVersionIdが分かれば、リレーションを辿ってContentDocumentIdを突き止める事が出来そうです。
では、以下の様に作戦を変更します。
新規作成レコードにファイルを紐付ける順序(改)
①ファイルアップロード時にContentVersionIdを取得する (ContentDocumentIdは、取れない..)
①’SOQLを使って、ContentVersionIdをWhere句に指定し、ContentDocumentIdを取得する
②ケースを登録し、CaseIdを取得する
③DocumentIdとCaseIdを紐づけるContentDocumentLinkを挿入する
7. ContentDocumentLinkによるレコードとファイルの関連付け処理
先ほどアップロードしたファイルのContentVersionIdと、適当な新規作成ケースのIdを引数に指定して、紐付けられるか実験してみます。
ContentVersionオブジェクトの中から、ContentVersionIdを指定して対象のレコードを取り出します。①'~③の実証実験です。
以下コードのSOQL部は、取り出したレコードの1属性であるContentDocumentIdを取得しています。
リファレンスによると、「外部ユーザは Visibility を AllUsers にのみ設定できます」とあるので、選択の余地はありません。
ケースIdと、ContentDocumentIdを指定してContentDocumentLinkレコードを作成する流れです。
Apexを実行。ケースレコード画面をリロードすると。。。意図したとおりにファイルが紐付いています。良かった良かった
これで、①~③を連続して処理することでケース作成&アップロード済みファイルの関連付けが出来ることが証明されました。
public without sharing class AttachmentController {
public static void CreateContentDocumentLink(Case newCase, List<String> documents) {
//ケース登録
insert newCase;
//紐づけオブジェクト宣言
List<ContentDocumentLink> newLinks = new List<ContentDocumentLink>();
//添付ファイル分ループ
for(String docverId : documents) {
//ContentVersionIdを元に、CondentDocumentIdを取得
ContentVersion cv = [SELECT Id,ContentDocumentId FROM ContentVersion WHERE Id =:docverId];
ContentDocumentLink newLink = new ContentDocumentLink(
ContentDocumentId = cv.ContentDocumentId,
LinkedEntityId = newCase.Id, //発番されたケースID
Visibility = 'AllUsers',
ShareType = 'v'
);
newLinks.add(newLink);
}
//紐づけオブジェクト挿入
insert newLinks;
}
}
8. ゲストユーザへApexClassの実行を許可
ケース登録の流れでゲストユーザがApexを実行出来る様に、プロファイルへ直接対象Classの実行権限を与えます。
「ユーザ > プロファイル」からゲスト用プロファイルを探すと、何故か対象プロファイルは見つかりません。
お気づきの方も居るかと思いますが、2章のエクスペリエンスビルダーの公開アクセス設定の下に、ゲスト用プロファイルへの入口があります。
9. Aura Componentでケースフォーム画面準備
メールアドレス、氏名、件名、説明の4つの入力ボックスのあるフォームを用意します。
以下画像の様な感じにします。
フォーム内に、アップロードコンポーネントを含めているのがポイントです。
Aura Componentの記載は、こんな感じです。
<aura:component implements="forceCommunity:availableForAllPageTypes"
access="global"
controller="AttachmentController">
<aura:attribute name="newCase" type="Case" default="{ 'Type': 'Question',
'Origin':'コミュニティ',
'Status':'New',
'SuppliedEmail' : ''},
'SuppliedName' : ''},
'Subject' : ''},
'Description' : ''}"/>
<aura:attribute name="documents" type="List" default="[]"/>
<aura:attribute name="filenames" type="List" default="[]"/>
<aura:attribute name="accept" type="List" default="['.jpg', '.jpeg', '.png', '.gif', '.bmp','.xls','.xlsx','.zip','.lzh']"/>
<aura:attribute name="multiple" type="Boolean" default="true"/>
<aura:attribute name="disabled" type="Boolean" default="false"/>
<aura:attribute name="isComplete" type="Boolean" default="false"/>
<article class="slds-card slds-m-horizontal_medium ">
<div class="slds-grid slds-einstein-header slds-card__header">
<header class="slds-media slds-media_center slds-has-flexi-truncate">
<div class="slds-grid slds-grid_vertical-align-center slds-size_3-of-4 slds-medium-size_2-of-3">
<div class="slds-media__body">
<h2 class="slds-truncate" title="Einstein (10+)">
<span class="slds-text-heading_small">MKIサポートセンター</span>
</h2>
</div>
</div>
<div class="slds-einstein-header__figure slds-size_1-of-4 slds-medium-size_1-of-3"></div>
</header>
</div>
<aura:if isTrue="{!not(v.isComplete)}">
<div class="Create slds-m-horizontal_medium slds-m-top_medium">
<lightning:card class="headerLabel" iconName="action:new_case" title="お問い合わせフォーム">
<form class="slds-form--stacked">
<lightning:input aura:id="SuppliedEmail"
label="メールアドレス"
name="SuppliedEmail"
value="{!v.newCase.SuppliedEmail}"
required="true"
class="input slds-m-horizontal_large slds-m-vertical_medium slds-m-top_x-large"
maxlength="80"/>
<lightning:input aura:id="SuppliedName"
label="氏名"
name="SuppliedName"
value="{!v.newCase.SuppliedName}"
required="true"
class="input slds-m-horizontal_large slds-m-vertical_medium"
maxlength="80"/>
<lightning:input aura:id="Subject"
label="件名"
name="Subject"
value="{!v.newCase.Subject}"
required="true"
class="input slds-m-horizontal_large slds-m-vertical_medium"
maxlength="80"/>
<lightning:textarea aura:id="Description"
name="Description"
label="説明"
value="{!v.newCase.Description}"
class="input slds-m-horizontal_large slds-m-top_medium"
required="true"/>
<div class="input slds-m-horizontal_large">
<lightning:fileUpload name="fileUploader"
multiple="{!v.multiple}"
accept="{!v.accept}"
disabled="{!v.disabled}"
onuploadfinished="{!c.onUploadFinished }"/>
</div>
<aura:iteration items="{!v.filenames}" var="doc">
<p class="slds-m-horizontal_large slds-text-title_bold ">{!doc}</p>
</aura:iteration>
<div class="slds-m-vertical_large slds-align_absolute-center">
<lightning:button label="登録"
class="communityBtn slds-button custombtn"
variant="brand"
onclick="{!c.SubmitCase}"/>
</div>
</form>
</lightning:card>
</div>
<aura:set attribute="else">
<p class="SuccessPanel">
<lightning:card title="" class="SuccessPanel vertical-center">
<div class="slds-list_horizontal slds-align_absolute-center slds-m-bottom_large">
<lightning:icon iconName="utility:success" alternativeText="Success!" variant="Success"
title="success variant xx-small" size="xx-small" />
<h2 class="slds-text-title_bold"> 正常に送信されました</h2>
</div>
<h2 class = "slds-m-bottom_large">弊社サポートよりご回答申し上げますので、今しばらくお待ち下さい</h2>
<lightning:button label="閉じる" title="close button" class="slds-m-left_x-small buttonwide"/>
</lightning:card>
</p>
</aura:set>
</aura:if>
<footer class="slds-card__footer">
</footer>
</article>
</aura:component>
ファイルをアップロードする度に、コントローラーのonUploadFinishedを実行しています。
登録ボタンをクリックすると、コントローラーのSubmitCase経由で、Apexクラス(ケースとファイルの紐付け)を実行する仕掛けです。
ケースとファイルの紐付けまで正常に終了すると、aura ifによりthank youコンポーネントに切り替えます。
10. Auraコントローラ処理内容
コントローラには、コンポーネントから呼び出される関数を2つ定義します。
一つ目は、アップロードが完了する度に呼ばれるonUploadFinishedです。
アップロードされたファイルのDocumentVersionIdを、後で紐付ける為にリストに追加していきます。
今現在どのファイルがアップロード済みなのかをユーザに表示する為に、ファイル名もリストに追加します。
これらリストは、コンポーネントアトリビュートとして定義されています。
(一度アップロードしたファイルのキャンセル処理は省略)
onUploadFinished: function (component, event, helper) {
var uploadedFiles = event.getParam("files");
//ファイル-レコード紐付け用
var documentVersionIds = component.get("v.documents");
uploadedFiles.forEach(element => documentVersionIds.push(element.contentVersionId));
component.set("v.documents", documentVersionIds);
//画面表示用ファイル名リスト追加
var filenames = component.get("v.filenames");
uploadedFiles.forEach(element => filenames.push("・"+element.name));
component.set("v.filenames", filenames);
}
2つ目は、登録ボタンを押下した際に呼ばれるsubmit処理です。
ケースを登録し、発番されたCase IdとDocumentVersionIdのリストをApex Classに渡します。
紐付けが完了すると、正常メッセージを表示する為に、切替フラグisCompleteにTrueをセットしています。
フラグがTrueになると、aura ifによりコンポーネントが自動的に切り替わります。
SubmitCase : function(component, event, helper) {
var action = component.get("c.CreateContentDocumentLink");
action.setParams ({
documents : component.get("v.documents"), //アップロードファイルリスト
newCase : component.get("v.newCase") //ケースオブジェクト
});
action.setCallback(this, function(response) {
var state = response.getState();
if (state==="SUCCESS") {
//aura:ifのフラグ設定、正常終了メッセージを表示
component.set("v.isComplete", "true");
}
else {
let errors = response.getError();
let message = 'エラーが発生しました';
if (errors && Array.isArray(errors) && errors.length > 0) {
message = errors[0].message;
}
console.error(message);
}
});
$A.enqueueAction(action);
}
11. 最後に
作ったコンポーネントを、公開サイトに埋め込んで動作を確認します。
以下アニメーションの様に、ゲストユーザによりレコードが作成され、同時にアップロードファイルが無事紐付きました。
公開サイトは、不特定多数の人がアクセス出来る為、セキュリティ設計を行う必要があります。
セキュリティの考慮は、Salesforceのベストプラクティスを参考にして下さい。
今回のサンプルコードは、リンクからダウンロードして試せます。
$\tiny{※この実験サンプルコードによよって生じた損害等に関して、一切の責任を負いかねます}$