mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
349 lines
14 KiB
JavaScript
349 lines
14 KiB
JavaScript
/**
|
|
* Flow Screen Component Template
|
|
*
|
|
* Demonstrates complete Flow integration patterns:
|
|
* - @api properties with inputOnly/outputOnly roles
|
|
* - FlowAttributeChangeEvent for output binding
|
|
* - FlowNavigationFinishEvent for programmatic navigation
|
|
* - availableActions for navigation state awareness
|
|
* - @AuraEnabled Apex integration for data operations
|
|
*
|
|
* @see https://developer.salesforce.com/docs/component-library/bundle/lightning-flow-support
|
|
*/
|
|
import { LightningElement, api, wire } from 'lwc';
|
|
import { FlowAttributeChangeEvent, FlowNavigationFinishEvent } from 'lightning/flowSupport';
|
|
|
|
// Apex imports (uncomment and modify as needed)
|
|
// import getRecords from '@salesforce/apex/FlowScreenController.getRecords';
|
|
// import processRecord from '@salesforce/apex/FlowScreenController.processRecord';
|
|
|
|
export default class FlowScreenComponent extends LightningElement {
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// FLOW INPUT PROPERTIES (inputOnly - Flow → Component)
|
|
// These receive values FROM the Flow. Changes in Flow update the component.
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Record ID from Flow context (e.g., from Record-Triggered Flow)
|
|
* Maps to: Flow variable or $Record.Id
|
|
*/
|
|
@api recordId;
|
|
|
|
/**
|
|
* Object API Name for context-aware behavior
|
|
* Maps to: Flow variable or $Record.Object
|
|
*/
|
|
@api objectApiName;
|
|
|
|
/**
|
|
* Custom label configurable in Flow Builder
|
|
* Maps to: Flow constant, variable, or direct text
|
|
*/
|
|
@api inputLabel = 'Select a Record';
|
|
|
|
/**
|
|
* Available navigation actions - automatically populated by Flow runtime
|
|
* Possible values: ['NEXT', 'BACK', 'FINISH', 'PAUSE']
|
|
* Use this to conditionally show/hide navigation buttons
|
|
*/
|
|
@api availableActions = [];
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// FLOW OUTPUT PROPERTIES (outputOnly - Component → Flow)
|
|
// These send values BACK to the Flow. Use FlowAttributeChangeEvent to update.
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Selected record ID - output to Flow
|
|
* IMPORTANT: Use FlowAttributeChangeEvent to notify Flow of changes
|
|
*/
|
|
@api selectedRecordId;
|
|
|
|
/**
|
|
* Selected record name - output to Flow
|
|
*/
|
|
@api selectedRecordName;
|
|
|
|
/**
|
|
* Boolean flag indicating completion - useful for Flow decisions
|
|
*/
|
|
@api isComplete = false;
|
|
|
|
/**
|
|
* Error message if validation fails - Flow can use in fault path
|
|
*/
|
|
@api errorMessage;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// PRIVATE REACTIVE PROPERTIES
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
records = [];
|
|
error;
|
|
isLoading = true;
|
|
_selectedId;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// LIFECYCLE HOOKS
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
connectedCallback() {
|
|
// Initialize component when added to DOM
|
|
this.loadData();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// DATA LOADING
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Load data - replace with @wire or imperative Apex call
|
|
*/
|
|
async loadData() {
|
|
this.isLoading = true;
|
|
try {
|
|
// Example: Load records from Apex
|
|
// this.records = await getRecords({ parentId: this.recordId });
|
|
|
|
// Placeholder data for template
|
|
this.records = [
|
|
{ id: '001xx000000001', name: 'Sample Record 1', description: 'Description 1' },
|
|
{ id: '001xx000000002', name: 'Sample Record 2', description: 'Description 2' },
|
|
{ id: '001xx000000003', name: 'Sample Record 3', description: 'Description 3' }
|
|
];
|
|
this.error = undefined;
|
|
} catch (err) {
|
|
this.error = this.reduceErrors(err);
|
|
this.records = [];
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// FLOW OUTPUT - FlowAttributeChangeEvent
|
|
// CRITICAL: This is how you send values back to Flow variables
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Handle record selection and notify Flow
|
|
* @param {Event} event - Click event from record tile
|
|
*/
|
|
handleRecordSelect(event) {
|
|
const recordId = event.currentTarget.dataset.id;
|
|
const recordName = event.currentTarget.dataset.name;
|
|
|
|
// Update local state
|
|
this._selectedId = recordId;
|
|
this.selectedRecordId = recordId;
|
|
this.selectedRecordName = recordName;
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// CRITICAL: Dispatch FlowAttributeChangeEvent for EACH output
|
|
// This notifies the Flow runtime that output values have changed
|
|
// The first parameter is the @api property name (case-sensitive)
|
|
// The second parameter is the new value
|
|
// ─────────────────────────────────────────────────────────────────
|
|
this.dispatchEvent(new FlowAttributeChangeEvent(
|
|
'selectedRecordId',
|
|
this.selectedRecordId
|
|
));
|
|
|
|
this.dispatchEvent(new FlowAttributeChangeEvent(
|
|
'selectedRecordName',
|
|
this.selectedRecordName
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Update completion status and notify Flow
|
|
* @param {Boolean} complete - Whether the screen is complete
|
|
*/
|
|
setComplete(complete) {
|
|
this.isComplete = complete;
|
|
this.dispatchEvent(new FlowAttributeChangeEvent('isComplete', complete));
|
|
}
|
|
|
|
/**
|
|
* Set error message and notify Flow
|
|
* @param {String} message - Error message
|
|
*/
|
|
setError(message) {
|
|
this.errorMessage = message;
|
|
this.dispatchEvent(new FlowAttributeChangeEvent('errorMessage', message));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// FLOW NAVIGATION - FlowNavigationFinishEvent
|
|
// Programmatically trigger Flow navigation (Next, Back, Finish, Pause)
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Navigate to next screen
|
|
* Only works if 'NEXT' is in availableActions
|
|
*/
|
|
handleNext() {
|
|
// Validate before navigation
|
|
if (!this.selectedRecordId) {
|
|
this.setError('Please select a record before proceeding.');
|
|
return;
|
|
}
|
|
|
|
// Clear any previous errors
|
|
this.setError(null);
|
|
|
|
// Mark as complete
|
|
this.setComplete(true);
|
|
|
|
// Check if NEXT action is available
|
|
if (this.canGoNext) {
|
|
// Dispatch navigation event - Flow will handle the rest
|
|
this.dispatchEvent(new FlowNavigationFinishEvent('NEXT'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate to previous screen
|
|
* Only works if 'BACK' is in availableActions
|
|
*/
|
|
handleBack() {
|
|
if (this.canGoBack) {
|
|
this.dispatchEvent(new FlowNavigationFinishEvent('BACK'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finish the flow (for final screens)
|
|
* Only works if 'FINISH' is in availableActions
|
|
*/
|
|
handleFinish() {
|
|
// Validate before finishing
|
|
if (!this.selectedRecordId) {
|
|
this.setError('Please select a record before finishing.');
|
|
return;
|
|
}
|
|
|
|
this.setComplete(true);
|
|
|
|
if (this.canFinish) {
|
|
this.dispatchEvent(new FlowNavigationFinishEvent('FINISH'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause the flow (for pausable flows)
|
|
* Only works if 'PAUSE' is in availableActions
|
|
*/
|
|
handlePause() {
|
|
if (this.canPause) {
|
|
this.dispatchEvent(new FlowNavigationFinishEvent('PAUSE'));
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// APEX INTEGRATION (Optional)
|
|
// Use @AuraEnabled methods for complex operations
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Example: Process selection with Apex before navigation
|
|
*/
|
|
async handleProcessAndContinue() {
|
|
this.isLoading = true;
|
|
try {
|
|
// Call Apex to process the selection
|
|
// const result = await processRecord({
|
|
// recordId: this.selectedRecordId,
|
|
// operation: 'validate'
|
|
// });
|
|
|
|
// Simulate successful processing
|
|
const result = { success: true, message: 'Validated successfully' };
|
|
|
|
if (result.success) {
|
|
this.setComplete(true);
|
|
this.handleNext();
|
|
} else {
|
|
this.setError(result.message);
|
|
}
|
|
} catch (err) {
|
|
this.setError(this.reduceErrors(err));
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// COMPUTED PROPERTIES
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
get hasRecords() {
|
|
return this.records && this.records.length > 0;
|
|
}
|
|
|
|
get hasSelection() {
|
|
return !!this._selectedId;
|
|
}
|
|
|
|
get canGoBack() {
|
|
return this.availableActions.includes('BACK');
|
|
}
|
|
|
|
get canGoNext() {
|
|
return this.availableActions.includes('NEXT');
|
|
}
|
|
|
|
get canFinish() {
|
|
return this.availableActions.includes('FINISH');
|
|
}
|
|
|
|
get canPause() {
|
|
return this.availableActions.includes('PAUSE');
|
|
}
|
|
|
|
get showBackButton() {
|
|
return this.canGoBack;
|
|
}
|
|
|
|
get showNextButton() {
|
|
return this.canGoNext;
|
|
}
|
|
|
|
get showFinishButton() {
|
|
return this.canFinish && !this.canGoNext;
|
|
}
|
|
|
|
/**
|
|
* Compute selected class for record tiles
|
|
*/
|
|
getRecordClass(recordId) {
|
|
return this._selectedId === recordId
|
|
? 'slds-box slds-box_link slds-theme_shade slds-is-selected'
|
|
: 'slds-box slds-box_link';
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
// UTILITIES
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Reduce various error formats to a string
|
|
* @param {*} errors - Error object(s)
|
|
* @returns {String} - Formatted error message
|
|
*/
|
|
reduceErrors(errors) {
|
|
if (!Array.isArray(errors)) {
|
|
errors = [errors];
|
|
}
|
|
|
|
return errors
|
|
.filter(error => !!error)
|
|
.map(error => {
|
|
if (error.body?.message) return error.body.message;
|
|
if (error.message) return error.message;
|
|
return JSON.stringify(error);
|
|
})
|
|
.join('; ');
|
|
}
|
|
}
|