diff --git a/force-app/components/relatedList/classes/TAG_RelatedListController.cls b/force-app/components/relatedList/classes/TAG_RelatedListController.cls new file mode 100644 index 000000000..b5a4d8e48 --- /dev/null +++ b/force-app/components/relatedList/classes/TAG_RelatedListController.cls @@ -0,0 +1,97 @@ +public with sharing class TAG_RelatedListController { + /** + * @description: Returns a list of records + * @return List + **/ + @AuraEnabled(cacheable=true) + public static List getCRMRelatedList( + String parentId, + String objectApiName, + String relationField, + String parentRelationField, + String parentObjectApiName, + String dateField, + String filterConditions, + String orderConditions + ) { + Set parentRelationIds = getParentRelation(parentId, parentRelationField, parentObjectApiName); + if (parentRelationIds.isEmpty()) { + return new List(); + } + + String query = 'SELECT Id' + (String.isBlank(dateField) ? '' : ', ' + dateField); + query += ' FROM ' + objectApiName + ' WHERE '; + query += String.isNotBlank(filterConditions) ? filterConditions.trim() + ' AND ' : ''; + // query += relationField + ' IN (SELECT ' + parentRelationField + ' FROM ' + parentObjectApiName + ' WHERE Id = \'' + parentId + '\')'; + query += relationField + ' IN :parentRelationIds '; + query += 'ORDER BY ' + (orderConditions != null ? orderConditions : 'ID DESC'); + + List returnList = Database.query(query); + return returnList; + } + + @AuraEnabled + public static List getRelatedList( + List fieldNames, + String parentId, + String objectApiName, + String relationField, + String parentRelationField, + String parentObjectApiName, + String filterConditions + ) { + Set parentRelationIds = getParentRelation(parentId, parentRelationField, parentObjectApiName); + String query = 'SELECT '; + + //Appending fields to query string + for (String field : fieldNames) { + query += field + ', '; + } + + query = query.removeEndIgnoreCase(', ') + ' FROM ' + objectApiName + ' WHERE '; + query += String.isNotBlank(filterConditions) ? filterConditions.trim() + ' AND ' : ''; + // query += relationField + ' IN (SELECT ' + parentRelationField + ' FROM ' + parentObjectApiName + ' WHERE Id = \'' + parentId + '\')'; + query += relationField + ' IN :parentRelationIds ORDER BY ID DESC'; + + List returnList = Database.query(query); + return returnList; + } + + /** + * @description: Returns a set of Strings to be used in getRelatedList + * @return Set + */ + private static Set getParentRelation( + String parentId, + String parentRelationField, + String parentObjectApiName + ) { + Set parentRelationIds = new Set(); + String query = 'SELECT ' + parentRelationField + ' FROM ' + parentObjectApiName + ' WHERE Id = :parentId'; + + String relationId; + for (SObject sObj : Database.query(query)) { + relationId = getParentRelationId(sObj, parentRelationField); + if (relationId != null) + parentrelationIds.add(relationId); + } + + return parentRelationIds; + } + + /** + * @description: Get the parent relation Id from sObject. Will recursively walk through the field hierarchy + * @return String + */ + private static String getParentRelationId(Sobject obj, String parentRelationField) { + List relationHierarchy = parentRelationField.split('\\.'); + String fieldApiName = relationHierarchy.remove(0); + + if (relationHierarchy.isEmpty()) { + return (String) obj.get(fieldApiName); + } else { + return getParentRelationId(obj.getSObject(fieldApiName), String.join(relationHierarchy, '.')); + } + + } +} \ No newline at end of file diff --git a/force-app/components/relatedList/classes/TAG_RelatedListController.cls-meta.xml b/force-app/components/relatedList/classes/TAG_RelatedListController.cls-meta.xml new file mode 100644 index 000000000..998805a82 --- /dev/null +++ b/force-app/components/relatedList/classes/TAG_RelatedListController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.css b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.css new file mode 100644 index 000000000..05c6f892a --- /dev/null +++ b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.css @@ -0,0 +1,33 @@ +.headerContainer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.5rem; +} + +.headerLeft { + display: flex; + align-items: center; + cursor: pointer; +} + +.headerIcon { + width: 1.5rem; + height: 1.5rem; +} + +.headerTitle { + margin: 0 0.5rem; + line-height: 1.5rem; +} + +.headerToggle { + margin-left: 0.5rem; + line-height: 1.5rem; +} + +.newRecordButton { + align-self: center; + height: 2rem; + padding: 0 0.5rem; +} diff --git a/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.html b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.html new file mode 100644 index 000000000..77ac4cfa0 --- /dev/null +++ b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.html @@ -0,0 +1,82 @@ + diff --git a/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js new file mode 100644 index 000000000..bd3ad366b --- /dev/null +++ b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js @@ -0,0 +1,278 @@ +import { LightningElement, api, track, wire } from 'lwc'; +import getRelatedList from '@salesforce/apex/TAG_RelatedListController.getRelatedList'; +import { NavigationMixin } from 'lightning/navigation'; +import { getRecord } from 'lightning/uiRecordApi'; +import { getObjectInfo } from 'lightning/uiObjectInfoApi'; + +export default class TagRelatedList extends NavigationMixin(LightningElement) { + @api recordId; + @api objectApiName; + @api listTitle; // Title of the list. + @api iconName; // Displayed icon. + @api columnLabels; // Columns to be displayed. + @api displayedFields = null; + @api relatedObjectApiName; // Related object name. + @api relationField; // Lookup/master-detail field name. + @api parentRelationField; // Parent relationship field in the junction. + @api filterConditions; // Optional filter conditions. + @api headerColor; // Header background color. + @api dynamicUpdate = false; // Auto-refresh flag. + @api maxHeight = 20; // Max height in em. + @api clickableRows; // Enable row click navigation. + @api wireFields; + @api collapsedCount = 0; // Number of records to show when collapsed + @api popoverFields; //Popover additional fields (comma separated) + @api showNewRecordButton; + @api newRecordButtonLabel; // Button label for New Record button + + + @track relatedRecords; + @track isExpanded = false; // Accordion state + + @track popoverRecordData; // Holds the record data for the hovered row + @track showPopover = false; // Flag to conditionally display popover + hoverTimer; // Timer for delayed popover display + @track popoverPosition = { top: 0, left: 0 }; + + connectedCallback() { + this.wireFields = [this.objectApiName + '.Id']; + this.getList(); + } + + @wire(getObjectInfo, { objectApiName: '$relatedObjectApiName' }) + objectInfo; + + // Wire to refresh the list when the parent record changes + @wire(getRecord, { recordId: '$recordId', fields: '$wireFields' }) + getaccountRecord({ data, error }) { + if (data && this.dynamicUpdate === true) { + this.getList(); + } else if (error) { + // Handle error accordingly + } + } + + // Retrieve related records from Apex + getList() { + getRelatedList({ + fieldNames: this.apexFieldList, + parentId: this.recordId, + objectApiName: this.relatedObjectApiName, + relationField: this.relationField, + parentRelationField: this.parentRelationField, + parentObjectApiName: this.objectApiName, + filterConditions: this.filterConditions + }) + .then((data) => { + this.relatedRecords = data && data.length > 0 ? data : null; + }) + .catch((error) => { + console.log('An error occurred: ' + JSON.stringify(error, null, 2)); + }); + } + + // Toggle the accordion state + toggleAccordion() { + this.isExpanded = !this.isExpanded; + } + + // Handle row click event if clickableRows is enabled + handleRowClick(event) { + let recordIndex = event.currentTarget.dataset.value; + this.navigateToRecord(this.relatedRecords[recordIndex].Id); + } + + navigateToRecord(recordId) { + this[NavigationMixin.Navigate]({ + type: 'standard__recordPage', + attributes: { + recordId: recordId, + objectApiName: this.relatedObjectApiName, + actionName: 'view' + } + }); + } + + handleNewRecord(event) { + // Prevent the header's onclick from firing + event.stopPropagation(); + this[NavigationMixin.Navigate]({ + type: 'standard__objectPage', + attributes: { + objectApiName: this.relatedObjectApiName, + actionName: 'new' + } + }); + } + + // Compute records to display based on whether the list is expanded or collapsed + get displayedRecords() { + const records = this.listRecords; + if (!this.isExpanded && records.length > this.collapsedCount) { + return records.slice(0, this.collapsedCount); + } + return records; + } + + get displayedFieldList() { + return this.displayedFields ? this.displayedFields.replace(/\s/g, '').split(',') : []; + } + + // Transform the raw related records into a display-friendly format + get listRecords() { + let returnRecords = []; + if (this.relatedRecords) { + this.relatedRecords.forEach((dataRecord) => { + let recordFields = []; + this.displayedFieldList.forEach((key) => { + if (key !== 'Id') { + recordFields.push({ + label: key, + value: this.resolve(key, dataRecord) + }); + } + }); + returnRecords.push({ recordFields: recordFields, Id: dataRecord.Id }); + }); + } + return returnRecords; + } + + // Build the card title with record count + get cardTitle() { + const numRecords = this.relatedRecords ? this.relatedRecords.length : 0; + return `${this.listTitle} (${numRecords})`; + } + + get headerBackground() { + return this.headerColor + ? `background-color: ${this.headerColor}; border: 1px solid ${this.headerColor}; cursor: pointer;` + : 'cursor: pointer;'; + } + + get tableHeaderStyle() { + return `width: 100%; max-height: ${this.maxHeight}em`; + } + + get scrollableStyle() { + return `max-height: ${this.maxHeight}em`; + } + + // Prepare column labels array for rendering the header row + get fieldLabels() { + let labels = this.columnLabels + ? this.columnLabels.replace(/\s/g, '').split(',') + : []; + return labels.map((label, index, arr) => { + // Base style for every header cell. + let style = 'vertical-align: middle; text-align: left; padding: 4px 8px;'; + // Remove extra left padding for the first cell. + if (index === 0) { + style += 'padding-left: 5px;'; + } + // Remove extra right padding for the last cell. + if (index === arr.length - 1) { + style += 'padding-right: 0px;'; + } + return { + value: label, + headerStyle: style + }; + }); + } + + // Parse and combine the displayed fields and popoverFields strings into an array + get apexFieldList() { + // Get displayedFields (if any) + let displayed = this.displayedFields ? this.displayedFields.replace(/\s/g, '').split(',') : []; + // Get popoverFields (if any) + let popover = this.popoverFields ? this.popoverFields.replace(/\s/g, '').split(',') : []; + // Combine them and remove duplicates + let combined = Array.from(new Set([...displayed, ...popover])); + return combined; + } + + get icon() { + return this.iconName && this.iconName !== '' ? this.iconName : null; + } + + get toggleIcon() { + return this.isExpanded ? '▼' : '▶'; + } + + get showRecords() { + return this.relatedRecords && this.relatedRecords.length > 0 && this.isExpanded; + } + + resolve(path, obj) { + if (typeof path !== 'string') { + throw new Error('Path must be a string'); + } + + return path.split('.').reduce(function (prev, curr) { + return prev ? prev[curr] : null; + }, obj || {}); + } + + // Event handler for mouse enter on a record row + handleMouseEnter(event) { + const recordId = event.currentTarget.dataset.recordId; + const rect = event.currentTarget.getBoundingClientRect(); + // Adjusting the position slightly (you can fine‑tune the offsets) + this.popoverPosition = { + top: rect.top + 10, + left: rect.left + 10 + }; + this.hoverTimer = window.setTimeout(() => { + this.popoverRecordData = this.relatedRecords.find(rec => rec.Id === recordId); + this.showPopover = true; + }, 1500); + } + + // Event handler for mouse leave from a record row (or popover) + handleMouseLeave() { + window.clearTimeout(this.hoverTimer); + this.showPopover = false; + } + + // Getter to combine displayedFields with additional popoverFields + get combinedPopoverFields() { + return this.apexFieldList; + } + + // Getter to prepare an array of objects with localized field labels and values from the hovered record + get popoverFieldValues() { + // Ensure we have the record data and the object metadata + if (!this.popoverRecordData || !this.objectInfo.data) { + return []; + } + return this.combinedPopoverFields.map(fieldApiName => { + // Look up the localized label (if available); if not, fallback to field API name + let fieldLabel = this.objectInfo.data.fields[fieldApiName] ? + this.objectInfo.data.fields[fieldApiName].label : + fieldApiName; + return { + apiName: fieldLabel, // Using the localized label here instead of the API name + value: this.resolve(fieldApiName, this.popoverRecordData) + }; + }); + } + + // Getter for popover style + get popoverStyle() { + if (this.popoverPosition) { + // Get the host element's bounding rectangle + const containerRect = this.template.host.getBoundingClientRect(); + // Compute coordinates relative to the host container + const relativeLeft = this.popoverPosition.left - containerRect.left; + const relativeTop = this.popoverPosition.top - containerRect.top; + // Add a vertical offset (+20px) and change the transform to not shift upward + return `position: absolute; + top: ${relativeTop + 20}px; + left: ${relativeLeft}px; + z-index: 1000; + transform: translate(-50%, 0);`; + } + return ''; + } +} diff --git a/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js-meta.xml b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js-meta.xml new file mode 100644 index 000000000..2661c01cc --- /dev/null +++ b/force-app/components/relatedList/lwc/tagRelatedList/tagRelatedList.js-meta.xml @@ -0,0 +1,111 @@ + + + 62.0 + true + Dynamic Related List + Component to display a records related list + + lightning__RecordPage + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file