Github Link : https://github.com/AourLegacy/AourFactory/tree/Timeline-lwc
Step 2 : Creation d’un projet sur VSCodes avec la commande cmd + shift + p , SFDX : Create Project .
Step 3 : Cree un lightning web component : cmd + shift + p , SFDX : Create Lightning web component . avec les code suivant : Timeline.js :
import { LightningElement , api , wire } from 'lwc';
import { NavigationMixin } from 'lightning/navigation';
import { loadScript } from 'lightning/platformResourceLoader';
import shortDateFormat from '@salesforce/i18n/dateTime.shortDateFormat';
import LOCALE from '@salesforce/i18n/locale';
import getTimelineData from '@salesforce/apex/TimelineService.getTimelineRecords';
import getTimelineTypes from '@salesforce/apex/TimelineService.getTimelineTypes';
import d3JS from '@salesforce/resourceUrl/d3minified';
import momentJS from '@salesforce/resourceUrl/momentminified';
import APEX from '@salesforce/label/c.Timeline_Error_Apex';
import SETUP from '@salesforce/label/c.Timeline_Error_Setup';
import NO_DATA_HEADER from '@salesforce/label/c.Timeline_Error_NoDataHeader';
import NO_DATA_SUBHEADER from '@salesforce/label/c.Timeline_Error_NoDataSubHeader';
import JAVASCRIPT_LOAD from '@salesforce/label/c.Timeline_Error_JavaScriptResources';
import UNHANDLED from '@salesforce/label/c.Timeline_Error_Unhandled';
import DAYS from '@salesforce/label/c.Timeline_Label_Days';
import SHOWING from '@salesforce/label/c.Timeline_Label_Showing';
import ITEMS from '@salesforce/label/c.Timeline_Label_Items';
import FILTERS from '@salesforce/label/c.Timeline_Label_Filters';
import TYPE_LEGEND from '@salesforce/label/c.Timeline_Label_Filter_Type_Legend';
import DATE_RANGE_LEGEND from '@salesforce/label/c.Timeline_Label_Date_Range_Legend';
import FILE_TYPE from '@salesforce/label/c.Timeline_Label_Files';
import ALL_TYPES from '@salesforce/label/c.Timeline_Label_Filter_All_Types';
import BUTTON_APPLY from '@salesforce/label/c.Timeline_Label_Apply';
import BUTTON_CANCEL from '@salesforce/label/c.Timeline_Label_Cancel';
export default class Timelinelwc extends NavigationMixin(LightningElement) {
//Adminstrator accessible attributes in app builder
@api timelineTitle; //title for the lwc set as design attribute
@api preferredHeight; //height of the timeline set as design attribute
@api earliestRange; //How far back in time to go
@api latestRange; //How far into the future to go
@api zoomTo; //Zoom to current dat or latest activity
@api daysToShow; //number of days to plot for the default zoom
//Component calculated attributes
@api recordId; //current record id of lead, case, opportunity, contact or account
@api flexipageRegionWidth; //SMALL, MEDIUM and LARGE based on where the component is placed in App Builder templates
timelineStart; //Calculated based on the earliestRange
timelineEnd; //Calculated based on the latestRange
zoomStartDate; //Start date of the current zoom
zoomEndDate; //End date of the current zoom
localisedZoomStartDate; //Start date of the current zoom
localisedZoomEndDate; //End date of the current zoom
totalTimelineRecords = 0; //Total number of records returned
totalZoomedRecords = 0; //Total records in zoom
noData = false; //Boolean when no data is returned
noFilterData = false; //Boolean when no data is returned after filtering
isLoaded = false; //Boolean when timeline data is loaded
isError = false; //Boolean when there is an error
isMouseOver = false; //Boolean when mouse over is detected
mouseOverRecordId; //Current Id of the record being hovered over
mouseOverObjectAPIName; //API Name for the object being hovered over
mouseOverObjectNameLabel;
mouseOverDetailLabel;
mouseOverDetailValue;
mouseOverFallbackField;
mouseOverFallbackValue;
mouseOverTime;
filterValues = [];
startingFilterValues = [];
allFilterValues = [];
objectFilter = [];
isFilter;
isFilterUpdated;
isFilterLoaded = false;
illustrationVisibility = 'illustration-hidden'; //Toggles the class to show and hide the illustration component
illustrationHeader; //Header to display when an information box displays
illustrationSubHeader; //Sub Header to display when an info box appears
illustrationType; //Type of illustration to display, 'error' or 'no data'
label = {
DAYS,
SHOWING,
ITEMS,
FILTERS,
TYPE_LEGEND,
DATE_RANGE_LEGEND,
FILE_TYPE,
ALL_TYPES,
BUTTON_APPLY,
BUTTON_CANCEL
};
error = {
APEX,
SETUP,
NO_DATA_HEADER,
NO_DATA_SUBHEADER,
JAVASCRIPT_LOAD,
UNHANDLED
};
_timelineData = null;
_timelineHeight = null;
//These are the objects holding individual instances of the timeline
_d3timelineCanvas = null;
_d3timelineCanvasAxis = null;
_d3timelineCanvasMap = null;
_d3timelineCanvasMapAxis = null;
_d3brush = null;
//These are the d3 selections that allow us to modify the DOM
_d3timelineCanvasSVG = null;
_d3timelineCanvasAxisSVG = null;
_d3timelineMapSVG = null;
_d3timelineMapAxisSVG = null;
_d3timelineCanvasDIV = null;
_d3timelineCanvasMapDIV = null;
_d3LocalisedShortDateFormat = null;
_d3Rendered = false;
@wire(getTimelineTypes, { parentObjectId: '$recordId' })
wiredResult(result) {
if (result.data) {
const timelineTs = result.data;
for (let key in timelineTs) {
// eslint-disable-next-line no-prototype-builtins
if (timelineTs.hasOwnProperty(key)) {
this.filterValues.push(key);
let tempFilter = [];
tempFilter.label = timelineTs[key];
tempFilter.value = key;
this.objectFilter.push(tempFilter);
this.startingFilterValues.push(key);
this.allFilterValues.push(key);
}
}
this.isFilterLoaded = true;
} else if (result.error) {
let errorMessage = result.error.body.message;
this.processError('Error', this.error.APEX, errorMessage);
}
}
disconnectedCallback() {
window.removeEventListener('resize', this.debounce);
}
connectedCallback() {
this._timelineHeight = this.getPreferredHeight();
this._d3LocalisedShortDateFormat = this.userDateFormat();
}
renderedCallback() {
if (!this._d3Rendered) {
//set the height of the component as the height is dynamic based on the attributes
let timelineDIV = this.template.querySelector('div.timeline-canvas');
timelineDIV.setAttribute('style', 'height:' + this._timelineHeight + 'px');
Promise.all([loadScript(this, d3JS), loadScript(this, momentJS)])
.then(() => {
//Setup d3 timeline by manipulating the DOM and do it once only as render gets called many times
this._d3timelineCanvasDIV = d3.select(this.template.querySelector('div.timeline-canvas'));
this._d3timelineCanvasMapDIV = d3.select(this.template.querySelector('div.timeline-canvas-map'));
this._d3timelineCanvasSVG = d3
.select(this.template.querySelector('div.timeline-canvas'))
.append('svg');
this._d3timelineCanvasAxisSVG = d3
.select(this.template.querySelector('div.timeline-canvas-axis'))
.append('svg');
this._d3timelineMapSVG = d3.select(this.template.querySelector('div.timeline-map')).append('svg');
this._d3timelineMapAxisSVG = d3
.select(this.template.querySelector('div.timeline-map-axis'))
.append('svg');
this.processTimeline();
})
.catch((error) => {
this.processError('Error', this.error.JAVASCRIPT_LOAD, error);
});
this._d3Rendered = true;
}
let timelineSummary = this.template.querySelectorAll('span.timeline-summary-verbose');
if (timelineSummary !== undefined && timelineSummary !== null) {
for (let i = 0; i < timelineSummary.length; i++) {
timelineSummary[i].classList.add('timeline-summary-verbose-' + this.flexipageRegionWidth);
}
}
}
processTimeline() {
const me = this;
me.isError = false;
me.isLoaded = false;
me.illustrationVisibility = 'illustration-hidden';
me.noData = false;
const dateTimeFormat = new Intl.DateTimeFormat(LOCALE);
me.timelineStart = dateTimeFormat.format(moment().subtract(me.earliestRange, 'years'));
me.timelineEnd = dateTimeFormat.format(moment().add(me.latestRange, 'years'));
me._d3timelineCanvasSVG.selectAll('*').remove();
me._d3timelineCanvasAxisSVG.selectAll('*').remove();
me._d3timelineMapSVG.selectAll('*').remove();
me._d3timelineMapAxisSVG.selectAll('*').remove();
getTimelineData({ parentObjectId: me.recordId, earliestRange: me.earliestRange, latestRange: me.latestRange })
.then((result) => {
try {
if (result.length > 0) {
me.totalTimelineRecords = result.length;
//Process timeline records
me._timelineData = me.getTimelineRecords(result);
//Process timeline canvas
me._d3timelineCanvas = me.timelineCanvas();
const axisDividerConfig = {
tickFormat: '%d %b %Y',
innerTickSize: -me._d3timelineCanvas.SVGHeight,
translate: [0, me._d3timelineCanvas.SVGHeight],
tickPadding: 0,
ticks: 6,
class: 'axis-ticks'
};
me._d3timelineCanvasAxis = me.axis(
axisDividerConfig,
me._d3timelineCanvasSVG,
me._d3timelineCanvas
);
const axisLabelConfig = {
tickFormat: me._d3LocalisedShortDateFormat,
innerTickSize: 0,
tickPadding: 2,
translate: [0, 5],
ticks: 6,
class: 'axis-label'
};
me._d3timelineCanvasAxisLabel = me.axis(
axisLabelConfig,
me._d3timelineCanvasAxisSVG,
me._d3timelineCanvas
);
//Process timeline map
me._d3timelineMap = me.timelineMap();
me._d3timelineMap.redraw();
const mapAxisConfig = {
tickFormat: me._d3LocalisedShortDateFormat,
innerTickSize: 4,
tickPadding: 4,
ticks: 6,
class: 'axis-label'
};
me._d3timelineMapAxis = me.axis(mapAxisConfig, me._d3timelineMapAxisSVG, me._d3timelineMap);
me._d3brush = me.brush();
window.addEventListener(
'resize',
me.debounce(() => {
try {
if (me.template.querySelector('div.timeline-canvas').offsetWidth !== 0) {
me._d3timelineCanvas.x.range([
0,
me.template.querySelector('div.timeline-canvas').offsetWidth
]);
me._d3timelineMap.x.range([
0,
Math.max(me.template.querySelector('div.timeline-map').offsetWidth, 0)
]);
me._d3timelineCanvasAxis.redraw();
me._d3timelineCanvasAxisLabel.redraw();
me._d3timelineMap.redraw();
me._d3timelineMapAxis.redraw();
me._d3brush.redraw();
}
} catch (error) {
//stay silent
}
}, 200)
);
me.isLoaded = true;
} else {
me.processError('No-Data', me.error.NO_DATA_HEADER, me.error.NO_DATA_SUBHEADER);
}
} catch (error) {
me.processError('Error', me.error.UNHANDLED, error.message);
}
})
.catch((error) => {
let errorType = 'Error';
let errorHeading,
errorMessage = '--';
try {
errorMessage = error.body.message;
let customError = JSON.parse(errorMessage);
errorType = customError.type;
errorMessage = customError.message;
errorHeading = me.error.SETUP;
} catch (error2) {
//fails to parse message so is a generic apex error
errorHeading = me.error.APEX;
}
me.processError(errorType, errorHeading, errorMessage);
});
}
getTimelineRecords(result) {
let timelineRecords = {};
let timelineResult = [];
let timelineTimes = [];
result.forEach(function (record, index) {
let recordCopy = {};
recordCopy.recordId = record.objectId;
recordCopy.id = index;
recordCopy.label =
record.detailField.length <= 30 ? record.detailField : record.detailField.slice(0, 30) + '...';
recordCopy.time = moment(record.positionDateValue, 'YYYY-MM-DD HH:mm:ss').toDate();
recordCopy.week = moment(record.positionDateValue, 'YYYY-MM-DD').startOf('week');
recordCopy.objectName = record.objectName;
recordCopy.objectNameLabel = record.objectNameLabel;
recordCopy.positionDateField = record.positionDateField;
recordCopy.detailField = record.detailField;
recordCopy.detailFieldLabel = record.detailFieldLabel;
recordCopy.fallbackTooltipField = record.fallbackTooltipField;
recordCopy.fallbackTooltipValue = record.fallbackTooltipValue;
recordCopy.tooltipId = record.tooltipId;
recordCopy.tooltipObject = record.tooltipObject;
recordCopy.drilldownId = record.drilldownId;
recordCopy.type = record.type;
recordCopy.icon = record.icon;
recordCopy.iconBackground = record.iconBackground;
timelineResult.push(recordCopy);
timelineTimes.push(recordCopy.time);
});
timelineRecords.data = timelineResult;
timelineRecords.minTime = d3.min(timelineTimes);
timelineRecords.maxTime = d3.max(timelineTimes);
timelineRecords.requestRange = [
moment().subtract(this.earliestRange, 'years').toDate(),
moment().add(this.latestRange, 'years').toDate()
];
return timelineRecords;
}
timelineCanvas() {
const me = this;
const timelineCanvasDIV = this.template.querySelector('div.timeline-canvas');
const timelineCanvas = me._d3timelineCanvasSVG;
const timelineData = me._timelineData;
const timelineHeight = me._timelineHeight;
const width = timelineCanvasDIV.offsetWidth;
timelineCanvasDIV.setAttribute('style', 'max-height:' + timelineHeight + 'px');
timelineCanvas.SVGHeight = timelineHeight;
timelineCanvas.x = d3.scaleTime().domain(timelineData.requestRange).rangeRound([0, width]);
timelineCanvas.y = function (swimlane) {
return swimlane * 25 * 1 + (swimlane + 1) * 5;
};
timelineCanvas.width = width;
timelineCanvas.height = timelineHeight;
timelineCanvas.filter = function (d) {
if (me.isFilterLoaded === false || me.filterValues.includes(d.objectName)) {
return true;
}
return false;
};
timelineCanvas.redraw = function (domain) {
var i = 0;
var swimlane = 0;
if (domain) {
timelineCanvas.x.domain(domain);
}
let swimlanes = [];
const unitInterval = (timelineCanvas.x.domain()[1] - timelineCanvas.x.domain()[0]) / timelineCanvas.width;
let data = timelineData.data
.filter(function (d) {
d.endTime = new Date(d.time.getTime() + unitInterval * (d.label.length * 6 + 80));
return timelineCanvas.x.domain()[0] < d.endTime && d.time < timelineCanvas.x.domain()[1];
})
.filter(timelineCanvas.filter);
me.totalZoomedRecords = data.length;
data.sort(me.sortByValue('time'));
data.forEach(function (entry) {
for (i = 0, swimlane = 0; i < swimlanes.length; i++, swimlane++) {
if (entry.time > swimlanes[i]) break;
}
entry.swimlane = swimlane;
swimlanes[swimlane] = entry.endTime;
});
timelineCanvas.width = timelineCanvas.x.range()[1];
timelineCanvas.attr('width', timelineCanvas.width);
const svgHeight = Math.max(timelineCanvas.y(swimlanes.length), timelineHeight);
timelineCanvas.height = timelineHeight;
timelineCanvas.attr('height', svgHeight - 1);
timelineCanvas.SVGHeight = svgHeight;
timelineCanvas.data = timelineCanvas
.selectAll('[class~=timeline-canvas-record]')
.data(data, function (d) {
return d.id;
})
.attr('transform', function (d) {
return 'translate(' + timelineCanvas.x(d.time) + ', ' + timelineCanvas.y(d.swimlane) + ')';
});
timelineCanvas.records = timelineCanvas.data
.enter()
.append('g')
.attr('class', 'timeline-canvas-record')
.attr('transform', function (d) {
return 'translate(' + timelineCanvas.x(d.time) + ', ' + timelineCanvas.y(d.swimlane) + ')';
});
if (timelineCanvas.records.size() > 0) {
timelineCanvas.records
.append('rect')
.attr('class', 'timeline-canvas-icon-wrap')
.attr('style', function (d) {
let iconColour = '';
switch (d.type) {
case 'Call':
iconColour = '#48C3CC';
break;
case 'Task':
iconColour = '#4ac076';
break;
case 'Event':
iconColour = '#eb7092';
break;
case 'Email':
iconColour = '#95AEC5';
break;
case 'SNOTE':
iconColour = '#E6D478';
break;
default:
iconColour = d.iconBackground;
break;
}
return 'fill: ' + iconColour;
})
.attr('x', 0)
.attr('y', 0)
.attr('width', 24)
.attr('height', 24)
.attr('rx', 3)
.attr('ry', 3);
timelineCanvas.records
.append('image')
.attr('x', 1)
.attr('y', 1)
.attr('height', 22)
.attr('width', 22)
.attr('xlink:href', function (d) {
let iconImage = '';
switch (d.type) {
case 'Call':
iconImage = '/img/icon/t4v35/standard/log_a_call.svg';
break;
case 'Task':
iconImage = '/img/icon/t4v35/standard/task.svg';
break;
case 'Event':
iconImage = '/img/icon/t4v35/standard/event.svg';
break;
case 'Email':
iconImage = '/img/icon/t4v35/standard/email.svg';
break;
case 'SNOTE':
iconImage = '/img/icon/t4v35/standard/note.svg';
break;
default:
iconImage = d.icon;
break;
}
return iconImage;
});
timelineCanvas.records
.append('rect')
.attr('class', 'timeline-canvas-record-wrap')
.attr('x', 24 + 8)
.attr('y', 0)
.attr('height', 24)
.attr('rx', 3)
.attr('ry', 3);
timelineCanvas.records
.append('line')
.attr('class', 'timeline-canvas-record-line')
.attr('x1', 24)
.attr('y1', 12)
.attr('x2', 24 + 8)
.attr('y2', 12);
timelineCanvas.records
.append('text')
.attr('class', 'timeline-canvas-record-label')
.attr('x', 24 + 10)
.attr('y', 16)
.attr('font-size', 12)
.on('click', function (d) {
let drilldownId = d.recordId;
if (d.drilldownId !== '') {
drilldownId = d.drilldownId;
}
switch (d.objectName) {
case 'ContentDocumentLink':
me[NavigationMixin.Navigate]({
type: 'standard__namedPage',
attributes: {
pageName: 'filePreview'
},
state: {
selectedRecordId: d.recordId
}
});
break;
default:
me[NavigationMixin.Navigate]({
type: 'standard__recordPage',
attributes: {
recordId: drilldownId,
actionName: 'view'
}
});
break;
}
})
.on('mouseover', function (d) {
let tooltipId = d.recordId;
let tooltipObject = d.objectName;
if (d.tooltipId !== '') {
tooltipId = d.tooltipId;
tooltipObject = d.tooltipObject;
}
me.mouseOverObjectAPIName = tooltipObject;
me.mouseOverObjectNameLabel = d.objectNameLabel;
me.mouseOverRecordId = tooltipId;
me.mouseOverFallbackField = d.fallbackTooltipField;
me.mouseOverFallbackValue = d.fallbackTooltipValue;
me.mouseOverDetailLabel = d.detailFieldLabel;
me.mouseOverDetailValue = d.detailField;
me.mouseOverTime = moment(d.time).format('DD.MM.YYYY HH:mm');
me.isMouseOver = true;
let tooltipDIV = me.template.querySelector('div.tooltip-panel');
tooltipDIV.setAttribute(
'style',
'top:' +
(this.getBoundingClientRect().top - 30) +
'px ;left:' +
(this.getBoundingClientRect().right + 15) +
'px ;visibility:visible'
);
})
.on('mouseout', function () {
let tooltipDIV = me.template.querySelector('div.tooltip-panel');
tooltipDIV.setAttribute('style', 'visibility: hidden');
me.isMouseOver = false;
})
.text(function (d) {
return d.label;
});
}
timelineCanvas.data.exit().remove();
};
return timelineCanvas;
}
axis(axisConfig, targetSVG, target) {
const me = this;
const timelineCanvas = me._d3timelineCanvas;
targetSVG.attr('width', target.width);
let x_axis = d3
.axisBottom(target.x)
.tickSizeInner(axisConfig.innerTickSize)
.ticks(axisConfig.ticks)
.tickFormat(d3.timeFormat(axisConfig.tickFormat))
.tickPadding(axisConfig.tickPadding);
const axis = targetSVG
.insert('g', ':first-child')
.attr('class', axisConfig.class + '-' + me.flexipageRegionWidth)
.call(x_axis);
if (typeof axisConfig.translate === 'object') {
axis.attr('transform', function () {
return 'translate(' + axisConfig.translate[0] + ', ' + axisConfig.translate[1] + ')';
});
}
axis.redraw = function () {
targetSVG.attr('width', target.width);
if (axisConfig.class === 'axis-ticks') {
axisConfig.innerTickSize = -timelineCanvas.SVGHeight;
axisConfig.translate = [0, timelineCanvas.SVGHeight];
}
x_axis = x_axis.tickSizeInner(axisConfig.innerTickSize);
x_axis = x_axis.tickValues(axisConfig.tickValues);
if (typeof axisConfig.translate === 'object') {
axis.attr('transform', function () {
return 'translate(' + axisConfig.translate[0] + ', ' + axisConfig.translate[1] + ')';
});
}
axis.call(x_axis);
};
return axis;
}
processError(type, header, message) {
if (this.illustrationVisibility !== 'illustration') {
this.isLoaded = true;
this.illustrationVisibility = 'illustration';
this.illustrationHeader = header;
this.illustrationSubHeader = message;
switch (type) {
case 'No-Data':
this.illustrationType = 'Desert';
this.isError = false;
this.noData = true;
break;
case 'No-Filter-Data':
this.illustrationType = 'Desert';
this.isError = false;
this.noFilterData = true;
break;
case 'Setup-Error':
this.illustrationType = 'Setup';
this.isError = true;
this.noFilterData = false;
this.noData = false;
break;
default:
this.illustrationType = 'PageNotAvailable';
this.isError = true;
break;
}
}
}
getPreferredHeight() {
let height;
switch (this.preferredHeight) {
case '1 - Smallest':
height = 125;
break;
case '2 - Small':
height = 200;
break;
case '3 - Default':
height = 275;
break;
case '4 - Big':
height = 350;
break;
case '5 - Biggest':
height = 425;
break;
default:
height = 275;
break;
}
return height;
}
timelineMap() {
const me = this;
const timelineData = me._timelineData;
const timelineMapSVG = me._d3timelineMapSVG;
const timelineMap = timelineMapSVG;
const timelineMapDIV = me.template.querySelector('div.timeline-map');
timelineMap.x = d3.scaleTime().domain(timelineData.requestRange).range([0, timelineMapDIV.offsetWidth]);
timelineMap.y = function (swimlane) {
return Math.min(swimlane, 7) * 4 + 4;
};
timelineMap.filter = function (d) {
if (me.isFilterLoaded === false || me.filterValues.includes(d.objectName)) {
return true;
}
return false;
};
timelineMap.width = timelineMapDIV.offsetWidth;
timelineMap.height = timelineMapDIV.offsetHeight;
timelineMap.redraw = function () {
var i = 0;
var swimlane = 0;
let swimlanes = [];
const unitInterval = (timelineMap.x.domain()[1] - timelineMap.x.domain()[0]) / timelineMap.width;
let data = timelineData.data
.filter(function (d) {
d.endTime = new Date(d.time.getTime() + unitInterval * 10);
return true;
})
.filter(timelineMap.filter);
data.sort(me.sortByValue('time'));
// calculating vertical layout for displaying data
data.forEach(function (entry) {
for (i = 0, swimlane = 0; i < swimlanes.length; i++, swimlane++) {
if (entry.time > swimlanes[i]) break;
}
entry.swimlane = swimlane;
swimlanes[swimlane] = entry.endTime;
});
data = data.filter(function (d) {
if (d.swimlane < 8) {
return true;
}
return false;
});
timelineMap.width = timelineMap.x.range()[1];
timelineMapSVG.attr('width', timelineMap.width);
timelineMap.data = timelineMap
.selectAll('[class~=timeline-map-record]')
.data(data, function (d) {
return d.id;
})
.attr('transform', function (d) {
return 'translate(' + timelineMap.x(d.time) + ', ' + timelineMap.y(d.swimlane) + ')';
});
timelineMap.records = timelineMap.data
.enter()
.append('g')
.attr('class', 'timeline-map-record')
.attr('transform', function (d) {
return 'translate(' + timelineMap.x(d.time) + ', ' + timelineMap.y(d.swimlane) + ')';
});
timelineMap.records
.append('rect')
.attr('style', function () {
return 'fill: #98C3EE; stroke: #4B97E6';
})
.attr('width', 3)
.attr('height', 2)
.attr('rx', 0.2)
.attr('ry', 0.2);
if (data.length <= 0) {
me.processError('No-Filter-Data', me.error.NO_DATA_HEADER, me.error.NO_DATA_SUBHEADER);
}
};
return timelineMap;
}
brush() {
const me = this;
const d3timeline = me._d3timelineCanvas;
const timelineData = me._timelineData;
const timelineAxis = me._d3timelineCanvasAxis;
const timelineAxisLabel = me._d3timelineCanvasAxisLabel;
const timelineMap = me._d3timelineMap;
const timelineMapSVG = me._d3timelineMapSVG;
const timelineMapLayoutA = timelineMapSVG.append('g');
const timelineMapLayoutB = timelineMapLayoutA.append('g');
let emptySelectionStart;
let defaultZoomDate;
let startBrush;
let endBrush;
switch (this.zoomTo) {
//case 'Historical Date':
// TODO
case 'Last Activity':
defaultZoomDate = moment(timelineData.maxTime).toDate();
break;
default:
defaultZoomDate = new Date().getTime();
break;
}
if (me.zoomStartDate !== undefined) {
startBrush = moment(me.zoomStartDate, 'DD MMM YYYY').format('DD MMM YYYY');
endBrush = moment(me.zoomEndDate, 'DD MMM YYYY').format('DD MMM YYYY');
} else {
startBrush = moment(defaultZoomDate)
.subtract(me.daysToShow / 2, 'days')
.toDate();
endBrush = moment(defaultZoomDate)
.add(me.daysToShow / 2, 'days')
.toDate();
}
timelineMapLayoutB.append('g').attr('class', 'brush').attr('transform', 'translate(0, -1)');
const xBrush = d3.select(this.template.querySelector('div.timeline-map')).select('g.brush');
let brush = d3
.brushX()
.extent([
[0, 0],
[timelineMap.width, timelineMap.height]
])
.on('brush', brushed)
.on('start', brushStart)
.on('end', brushEnd);
const handle = xBrush
.selectAll('.handle--custom')
.data([{ type: 'w' }, { type: 'e' }])
.enter()
.append('path')
.attr('class', 'handle--custom')
.attr('fill', '#4b97e6')
.attr('fill-opacity', 0.8)
.attr('stroke', '#000')
.attr('height', 40)
.attr('stroke-width', 1)
.attr('cursor', 'ew-resize')
.attr(
'd',
'M0,0 L75,0 L75,176 C75,184.284271 68.2842712,191 60,191 L15,191 C6.71572875,191 1.01453063e-15,184.284271 0,176 L0,0 L0,0 Z'
);
xBrush.call(brush).call(brush.move, [new Date(startBrush), new Date(endBrush)].map(timelineMap.x));
brush.redraw = function () {
brush = d3
.brushX()
.extent([
[0, 0],
[timelineMap.width, timelineMap.height]
])
.on('brush', brushed)
.on('start', brushStart)
.on('end', brushEnd);
startBrush = moment(me.zoomStartDate, 'DD MMM YYYY').format('DD MMM YYYY');
endBrush = moment(me.zoomEndDate, 'DD MMM YYYY').format('DD MMM YYYY');
xBrush.call(brush).call(brush.move, [new Date(startBrush), new Date(endBrush)].map(timelineMap.x));
};
function brushed() {
const selection = d3.event.selection;
const dommy = [];
if (selection) {
dommy.push(timelineMap.x.invert(selection[0]));
dommy.push(timelineMap.x.invert(selection[1]));
d3timeline.redraw(dommy);
timelineAxis.redraw();
timelineAxisLabel.redraw();
handle.attr('transform', function (d, i) {
return 'translate(' + (selection[i] - 2) + ', ' + 0 + ') scale(0.05)';
});
me.daysToShow = moment(d3timeline.x.domain()[1]).diff(moment(d3timeline.x.domain()[0]), 'days');
const dateTimeFormat = new Intl.DateTimeFormat(LOCALE);
me.zoomStartDate = moment(timelineMap.x.invert(selection[0])).format('DD MMM YYYY');
me.zoomEndDate = moment(timelineMap.x.invert(selection[1])).format('DD MMM YYYY');
me.localisedZoomStartDate = dateTimeFormat.format(moment(timelineMap.x.invert(selection[0])));
me.localisedZoomEndDate = dateTimeFormat.format(moment(timelineMap.x.invert(selection[1])));
}
}
function brushStart() {
const selection = d3.event.selection;
if (selection) {
emptySelectionStart = timelineMap.x.invert(selection[0]);
handle.attr('transform', function (d, i) {
return 'translate(' + (selection[i] - 2) + ', ' + 0 + ') scale(0.05)';
});
}
}
function brushEnd() {
const selection = d3.event.selection;
if (selection === null) {
me.zoomStartDate = moment(emptySelectionStart).toDate();
me.zoomEndDate = moment(emptySelectionStart).add(14, 'days').toDate();
me._d3brush.redraw();
}
}
return brush;
}
debounce = (fn, time) => {
let timeout;
return function () {
const functionCall = () => fn.apply(this, arguments);
clearTimeout(timeout);
// eslint-disable-next-line @lwc/lwc/no-async-operation
timeout = setTimeout(functionCall, time);
};
};
sortByValue(param) {
return function (a, b) {
return a[param] < b[param] ? -1 : a[param] > b[param] ? 1 : 0;
};
}
userDateFormat() {
const userShortDate = shortDateFormat;
let d3DateFormat = userShortDate.replace(/dd/gi, 'd');
d3DateFormat = d3DateFormat.replace(/d/gi, 'd');
d3DateFormat = d3DateFormat.replace(/M/gi, 'm');
d3DateFormat = d3DateFormat.replace(/MM/gi, 'm');
d3DateFormat = d3DateFormat.replace(/YYYY/gi, 'y');
d3DateFormat = d3DateFormat.replace(/YY/gi, 'y');
d3DateFormat = d3DateFormat.replace(/d/gi, '%d');
d3DateFormat = d3DateFormat.replace(/m/gi, '%m');
d3DateFormat = d3DateFormat.replace(/y/gi, '%Y');
return d3DateFormat;
}
get showSummary() {
if (this.isError || this.noData || this.noFilterData || !this.isLoaded) {
return false;
}
return true;
}
get showFallbackTooltip() {
if (this.mouseOverFallbackField != null && this.mouseOverFallbackField !== '') {
return true;
}
return false;
}
toggleFilter() {
const filterPopover = this.template.querySelector('div.timeline-filter');
const filterClasses = String(filterPopover.classList);
const refreshButton = this.template.querySelector('lightning-button-icon.timeline-refresh');
if (filterClasses.includes('slds-is-open')) {
refreshButton.disabled = false;
filterPopover.classList.remove('slds-is-open');
this.isFilter = false;
} else {
refreshButton.disabled = true;
filterPopover.classList.add('slds-is-open');
this.isFilter = true;
}
}
get filterOptions() {
this.handleAllTypes();
return this.objectFilter;
}
get selectedFilterValues() {
return this.filterValues.join(', ');
}
handleFilterChange(e) {
this.filterValues = e.detail.value;
this.handleAllTypes();
this.isFilterUpdated = false;
if (JSON.stringify(this.filterValues) !== JSON.stringify(this.startingFilterValues)) {
this.isFilterUpdated = true;
}
}
handleAllTypesChange(e) {
if (e.target.checked === true) {
this.filterValues = this.allFilterValues;
} else if (e.target.checked === false) {
this.filterValues = [];
}
this.isFilterUpdated = false;
if (JSON.stringify(this.filterValues) !== JSON.stringify(this.startingFilterValues)) {
this.isFilterUpdated = true;
}
}
handleAllTypes() {
const allTypesCheckbox = this.template.querySelector('input.all-types-checkbox');
const countAllValues = this.allFilterValues.length;
const countSelectedValues = this.filterValues.length;
if (countSelectedValues !== countAllValues && countSelectedValues > 0) {
allTypesCheckbox.checked = false;
allTypesCheckbox.indeterminate = true;
}
if (countSelectedValues === countAllValues) {
allTypesCheckbox.indeterminate = false;
allTypesCheckbox.checked = true;
}
if (countSelectedValues < 1) {
allTypesCheckbox.indeterminate = false;
allTypesCheckbox.checked = false;
}
}
applyFilter() {
this.refreshTimeline();
this.isFilterUpdated = false;
this.startingFilterValues = this.filterValues;
this.toggleFilter();
}
cancelFilter() {
this.filterValues = this.startingFilterValues;
this.isFilterUpdated = false;
this.toggleFilter();
}
refreshTimeline() {
this.isError = false;
this.noFilterData = false;
if (this.totalTimelineRecords > 0) {
this.illustrationVisibility = 'illustration-hidden';
this._d3timelineMapSVG.selectAll('[class~=timeline-map-record]').remove();
this._d3timelineMap.redraw();
this._d3brush.redraw();
}
}
}
Timeline.css
.stencil-label {
background-color: #e4e0e0;
height: 0.4rem;
border-radius: 2rem;
width: 10rem;
}
.stencil-value {
background-color: #c5c2c2;
height: 0.4rem;
border-radius: 2rem;
margin-top: 0.5rem;
width: 10rem;
}
.stencil-timeline {
text-align: center;
height: 70%;
position: relative;
top: 5%;
}
.stencil-timeline-spinner {
text-align: center;
}
.illustration {
padding: 10px 5px 5px 5px;
background-color: #fff;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.illustration-hidden {
display: none;
}
.timeline-container {
position: relative;
padding: 10px 5px 5px 5px;
background-color: #fff;
border-radius: 5px 5px 5px 5px;
border: rgb(221, 219, 218);
border-width: 1px;
border-style: solid;
}
.timeline-container-hidden {
padding: 10px 5px 5px 5px;
background-color: #fff;
border-radius: 5px 5px 5px 5px;
display: none;
}
.timeline-filter {
position: absolute;
top: 0;
right: 0;
}
.timeline-canvas-icon-wrap {
fill: #fff;
stroke: #d5dbe4;
stroke-width: 1;
}
.timeline-map-wrapper {
background-color: #f8fcff;
padding: 10px;
overflow: hidden;
}
.timeline-canvas-record-line {
stroke: #d5dbe4;
stroke-width: 1;
}
.timeline-canvas {
overflow-y: auto;
border-bottom: 1px solid #d5dbe4;
overflow-x: hidden;
}
.timeline-canvas-axis {
background-color: #fff;
border-bottom: 1px solid #d5dbe4;
height: 25px;
}
.timeline-canvas-axis line {
stroke: #72a8e0;
}
.timeline-map {
background-color: #fff;
border: 1px solid #4b97e6;
border-radius: 3px;
height: 40px;
}
.timeline-map svg {
height: 100%;
width: 100%;
}
.timeline-map-axis {
height: 20px;
}
.timeline-map-axis svg {
overflow: visible;
}
/* axis labels */
.axis-ticks {
font-family: Salesforce Sans, sans-serif;
font-size: 10px;
}
.axis-ticks text {
display: none;
}
.axis-label-LARGE text {
color: rgb(62, 62, 60);
font-size: 0.7rem;
}
.axis-label-MEDIUM text {
color: rgb(62, 62, 60);
font-size: 0.7rem;
}
.axis-label-SMALL text {
color: rgb(62, 62, 60);
font-size: 0.7rem;
}
/* axis tick marks */
.axis-ticks line {
stroke-width: 1;
stroke: rgb(221, 224, 228);
stroke-dasharray: 9, 9;
shape-rendering: crispEdges;
}
.axis-ticks-LARGE line {
stroke-width: 1;
stroke: rgb(221, 224, 228);
stroke-dasharray: 9, 9;
shape-rendering: crispEdges;
}
.axis-ticks-MEDIUM line {
stroke-width: 1;
stroke: rgb(221, 224, 228);
stroke-dasharray: 9, 9;
shape-rendering: crispEdges;
}
.axis-ticks-SMALL line {
stroke-width: 1;
stroke: rgb(221, 224, 228);
stroke-dasharray: 9, 9;
shape-rendering: crispEdges;
}
.timeline-map-axis line {
stroke: #72a8e0;
}
.timeline-map-axis text {
fill: #72a8e0;
height: 10px;
color: #92bce4;
font-size: 0.7rem;
}
/* axis line */
.axis-ticks path {
stroke: none;
}
.axis-label-LARGE path {
stroke: none;
}
.axis-label-MEDIUM path {
stroke: none;
}
.axis-label-SMALL path {
stroke: none;
}
/* brush */
.brush .selection {
stroke: #61adfc;
fill: #61adfc;
fill-opacity: 0.3;
}
.timeline-canvas-record {
stroke-width: 6;
cursor: default;
pointer-events: true;
}
.timeline-canvas-record-label {
fill: rgb(63, 66, 71);
shape-rendering: crispEdges;
color: #080707;
font-size: 0.8125rem;
cursor: pointer;
}
.timeline-wrapper {
margin: 10px 10px 10px 10px;
border: 1px solid #d5dbe4;
border-radius: 3px;
position: relative;
}
.slds-text-heading_medium,
.slds-text-heading--medium {
font-weight: 500;
font-size: 1rem;
padding-left: 10px;
line-height: 1.25;
}
.slds-popover__header {
padding: 1rem 1rem 0.75rem;
}
.slds-popover {
border-radius: 0.25rem;
width: 25rem;
min-height: 2rem;
z-index: 98;
text-align: left;
background-color: #fff;
display: inline-block;
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.16);
border: 1px solid #dddbda;
}
.slds-is-relative.record-form-spinner-holder {
min-height: 120px;
}
.tooltip-panel {
background: rgb(243, 242, 242);
color: #fff;
position: fixed !important;
border-radius: 4px;
padding: 10px;
font-size: 12px;
z-index: 98;
min-width: 400px;
min-height: 150px;
visibility: hidden;
}
.tooltip-header {
color: var(--lwc-brandDark);
}
.tooltip-content {
overflow: hidden;
transition: 0.25s all ease;
transition-delay: 0s;
}
@media only screen and (max-width: 900px) {
.axis-label-LARGE .tick:nth-child(2n) text {
visibility: hidden;
}
.timeline-summary-verbose {
display: none;
}
}
@media only screen and (max-width: 1200px) {
.axis-label-MEDIUM .tick:nth-child(2n) text {
visibility: hidden;
}
}
.axis-label-SMALL .tick:nth-child(2n) text {
visibility: hidden;
}
.timeline-summary-verbose-SMALL {
display: none;
}
Timeline.html
<template>
<!--main timeline component-->
<div class="timeline-container">
<!--header-->
<div class="slds-grid slds-grid--align-spread">
<div class="slds-col slds-shrink slds-align-middle">
<template if:true={isLoaded}>
<h1 class="slds-text-heading--medium">{timelineTitle}</h1>
</template>
<template if:false={isLoaded}>
<div class="stencil-value" style="margin-left: 10px;"></div>
</template>
</div>
<div class="slds-col slds-shrink-none slds-align-middle">
<div class="slds-form--inline">
<div class="slds-form-element">
<template if:true={showSummary}>
<div class="timeline-summary">
<span class="timeline-summary-verbose">{label.SHOWING} </span>
<span>{localisedZoomStartDate} - {localisedZoomEndDate}</span>
<span class="timeline-summary-verbose">
• {daysToShow} {label.DAYS} • {totalZoomedRecords} {label.ITEMS}</span
>
</div>
</template>
<template if:false={isLoaded}>
<div class="stencil-label"></div>
</template>
</div>
<div class="slds-form-element">
<lightning-button-group>
<lightning-button-icon
class="timeline-refresh"
icon-name="utility:refresh"
alternative-text="Refresh"
onclick={processTimeline}
></lightning-button-icon>
<lightning-button-icon-stateful
icon-name="utility:filterList"
selected={isFilter}
onclick={toggleFilter}
alternative-text="Filter"
></lightning-button-icon-stateful>
</lightning-button-group>
</div>
</div>
</div>
</div>
<!--end header-->
<!--timeline body-->
<div id="timeline-wrapper" class="timeline-wrapper">
<div class="slds-col slds-size--1-of-1">
<div class="timeline-canvas" id="timeline-canvas" lwc:dom="manual"></div>
<div class="timeline-canvas-axis" id="timeline-canvas-axis" lwc:dom="manual"></div>
<div class="timeline-map-wrapper">
<div class="timeline-map" id="timeline-map" lwc:dom="manual"></div>
<div class="timeline-map-axis" id="timeline-map-axis" lwc:dom="manual"></div>
</div>
</div>
<!--illustration component when there is an error-->
<div class={illustrationVisibility}>
<div class="stencil-timeline">
<c-timeline-illustration
header={illustrationHeader}
sub-header={illustrationSubHeader}
type={illustrationType}
></c-timeline-illustration>
</div>
</div>
<!--end illustration component-->
<div
class="timeline-filter slds-float_right slds-panel slds-size_small slds-panel_docked slds-panel_docked-right"
aria-hidden="false"
>
<div class="slds-panel__header" style="height: 50px;">
<template if:false={isFilterUpdated}>
<h2 class="slds-panel__header-title slds-text-heading_small slds-truncate" title="Panel Header">
{label.FILTERS}
</h2>
<button
class="slds-button slds-button_icon slds-button_icon slds-button_icon-small slds-float_right slds-panel__close"
title="Close dialog"
onclick={toggleFilter}
>
<lightning-icon
icon-name="utility:close"
alternative-text="Close"
size="xx-small"
></lightning-icon>
</button>
</template>
<template if:true={isFilterUpdated}>
<div class="slds-grid" style="width: 100%;">
<div>
<lightning-button
label={label.BUTTON_CANCEL}
title="Cancel"
onclick={cancelFilter}
></lightning-button>
</div>
<div class="slds-grid slds-col slds-grid_align-end">
<lightning-button
variant="brand"
label={label.BUTTON_APPLY}
title="Apply"
onclick={applyFilter}
></lightning-button>
</div>
</div>
</template>
</div>
<div class="slds-panel__body">
<legend class="slds-form-element__label slds-text-title--caps">{label.TYPE_LEGEND}</legend>
<div class="slds-form-element">
<div class="slds-form-element__control">
<div class="slds-checkbox">
<input
type="checkbox"
class="all-types-checkbox"
name="options"
id="all-filter"
value={handleAllTypesLoad}
onchange={handleAllTypesChange}
/>
<label class="slds-checkbox__label" for="all-filter">
<span class="slds-checkbox_faux"></span>
<span class="slds-form-element__label">{label.ALL_TYPES}</span>
</label>
</div>
</div>
</div>
<template if:true={isFilter}>
<lightning-checkbox-group
name="Types Shown"
label=""
options={filterOptions}
value={filterValues}
onchange={handleFilterChange}
>
</lightning-checkbox-group>
</template>
<div style="padding-top: 10px;"></div>
<legend class="slds-form-element__label slds-text-title--caps">{label.DATE_RANGE_LEGEND}</legend>
<div class="slds-form-element__label" style="padding-top: 5px;">
{timelineStart} - {timelineEnd}
</div>
</div>
</div>
</div>
<!--end timeline body-->
<!--spinner when component is loading-->
<div class="stencil-timeline-spinner">
<template if:false={isLoaded}>
<lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
</template>
</div>
<!--end spinner-->
</div>
<!--tooltips FALLBACK WHEN AN OBJECT IS NOT SUPPORTED-->
<div aria-label="Tooltip popover" class="tooltip-panel slds-popover slds-popover_panel slds-nubbin_left-top">
<div class="tooltip-content">
<template if:true={isMouseOver}>
<div class="tooltip-header slds-text-heading_medium">{mouseOverTime}<br/><b>{mouseOverObjectNameLabel}</b></div>
<template if:false={showFallbackTooltip}>
<lightning-record-form
record-id={mouseOverRecordId}
object-api-name={mouseOverObjectAPIName}
layout-type="Compact"
columns="2"
mode="readonly"
class="tooltip-content"
>
</lightning-record-form>
</template>
<template if:true={showFallbackTooltip}>
<div class="slds-form" role="list">
<div class="slds-form__row">
<div class="slds-form__item" role="listitem">
<div class="slds-form-element slds-form-element_readonly slds-form-element_stacked">
<span class="slds-form-element__label">{mouseOverDetailLabel} </span>
<div class="slds-form-element__control">
<div class="slds-form-element__static">{mouseOverDetailValue}</div>
</div>
</div>
</div>
</div>
<div class="slds-form__row">
<div class="slds-form__item" role="listitem">
<div class="slds-form-element slds-form-element_readonly slds-form-element_stacked">
<span class="slds-form-element__label">{mouseOverFallbackField} </span>
<div class="slds-form-element__control">
<div class="slds-form-element__static">{mouseOverFallbackValue}</div>
</div>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
<!--end tooltips-->
</template>
Timeline.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Timeline</masterLabel>
<targets>
<target>lightning__RecordPage</target>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__RecordPage">
<objects>
<object>Contact</object>
<object>Lead</object>
<object>Account</object>
<object>Case</object>
<object>Opportunity</object>
</objects>
<property name="timelineTitle" label="Title" default="Timeline" required="true" type="String" />
<property name="preferredHeight" label="Height" default="3 - Default" required="true" type="String" datasource="1 - Smallest, 2 - Small, 3 - Default, 4 - Big, 5 - Biggest" />
<property name="earliestRange" label="Historical Time Range (Years)" default="3" required="true" type="String" datasource="0.25, 0.5, 1, 2, 3, 5" description="Specify the start range for the timeline. As n minus current date."/>
<property name="latestRange" label="Future Time Range (Years)" default="0.5" required="true" type="String" datasource="0.25, 0.5, 1" description="Specify the end range for the timeline. As n plus current date."/>
<property name="zoomTo" label="Zoom Based On" default="Current Date" required="true" type="String" datasource="Current Date, Last Activity" description="Zoom to the last record found (even if it's in the past or future) OR zoom to the current date (even if there are no records)."/>
<property name="daysToShow" label="Zoom Range (Days)" default="60" required="true" type="Integer" min="7" max="120" description="Specify how many days to zoom to by default. e.g. 60 days would show 30 days prior to and 30 days after the specified 'zoom based on' setting."/>
<supportedFormFactors>
<supportedFormFactor type="Large" />
</supportedFormFactors>
</targetConfig>
<targetConfig targets="lightningCommunity__Default">
<property name="timelineTitle" label="Title" default="Timeline" required="true" type="String" />
<property name="preferredHeight" label="Height" default="3 - Default" required="true" type="String" datasource="1 - Smallest, 2 - Small, 3 - Default, 4 - Big, 5 - Biggest" />
<property name="earliestRange" label="Historical Time Range (Years)" default="3" required="true" type="String" datasource="0.25, 0.5, 1, 2, 3, 5" description="Specify the start range for the timeline. As n minus current date."/>
<property name="latestRange" label="Future Time Range (Years)" default="0.5" required="true" type="String" datasource="0.25, 0.5, 1" description="Specify the end range for the timeline. As n plus current date."/>
<property name="zoomTo" label="Zoom Based On" default="Current Date" required="true" type="String" datasource="Current Date, Last Activity" description="Zoom to the last record found (even if it's in the past or future) OR zoom to the current date (even if there are no records)."/>
<property name="daysToShow" label="Zoom Range (Days)" default="60" required="true" type="Integer" min="7" max="120" description="Specify how many days to zoom to by default. e.g. 60 days would show 30 days prior to and 30 days after the specified 'zoom based on' setting."/>
<property name="recordId" label="Record Id" default="{!recordId}" required="true" type="String" datasource="{!recordId}" description="Automatically binds the page's record id to the component variable" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Step 4 : Cree un class Apex : cmd + shift + p : Create Apex Class Avec le code suivant :
/**
*
* https://github.com/deejay-hub/timeline-lwc with modifications
*
* @description Apex supporting methods for the timeline lightning web component
*/
public without sharing class TimelineService {
/**
* @description Return all child record types for the parent record in context
* @param parentObjectId Id of the parent object used as the basis for the query
* @return A map of the API name and label or each child record type to plot on the timeline
*/
@AuraEnabled(Cacheable=true)
public static Map<String, String> getTimelineTypes( String parentObjectId ) {
try {
String parentObjectType = String.valueOf(Id.valueOf(parentObjectId).getSobjectType());
String queryTimelineConfiguration = 'SELECT Active__c, '
+ 'Object_Name__c, '
+ 'ObjectNamePluralLabel__c, '
+ 'Tooltip_Object_Name__c, '
+ 'Sequence__c '
+ 'FROM Timeline_Configuration__mdt '
+ 'WHERE Active__c = true AND '
+ 'Parent_Object__c =:parentObjectType '
+ 'ORDER BY Sequence__c ASC '; //NOPMD
List<Timeline_Configuration__mdt> listOfTimelineConfigurations = Database.query( queryTimelineConfiguration ); //NOPMD
Map<String, String> mapOfTimelineTypes = new Map<String, String>();
for ( Timeline_Configuration__mdt timelineType : listOfTimelineConfigurations ) {
String objectLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineType.Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabelPlural();
if ( timelineType.Object_Name__c == 'ContentDocumentLink') {
objectLabel = System.Label.Timeline_Label_Files;
}
if ( timelineType.Tooltip_Object_Name__c != null && timelineType.Tooltip_Object_Name__c != '') {
objectLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineType.Tooltip_Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabelPlural();
}
if (timelineType.ObjectNamePluralLabel__c != null && timelineType.ObjectNamePluralLabel__c != ''){
objectLabel = timelineType.ObjectNamePluralLabel__c;
}
mapOfTimelineTypes.put(timelineType.Object_Name__c, objectLabel);
}
return mapOfTimelineTypes;
}
catch(Exception e) {
throw new AuraHandledException(e.getMessage() + ' : ' + e.getStackTraceString());
}
}
@AuraEnabled
/**
* @description Return all child records for the parent record in context based on those active in Timeline_Configuration__mdt
* @param parentObjectId The id of the parent record
* @param earliestRange The number of historical years to include in the query
* @param latestRange The number of years in the future to include in the query
* @return A map of API Object Names and their corresponding translated labels
*/
public static List<Map<String, String>> getTimelineRecords( String parentObjectId, String earliestRange, String latestRange ) {
try {
String parentObjectType = String.valueOf(Id.valueOf(parentObjectId).getSobjectType());
String parentConfigType = parentObjectType;
earliestRange = String.valueOf((Decimal.valueOf(earliestRange) * 12).intValue());
latestRange = String.valueOf((Decimal.valueOf(latestRange) * 12).intValue());
String queryTimelineConfiguration = 'SELECT Detail_Field__c, '
+ 'Detail_Field_to_Label__c, '
+ 'Relationship_Name__c, '
+ 'Active__c, '
+ 'Icon__c, '
+ 'Icon_Background_Colour__c, '
+ 'Position_Date_Field__c, '
+ 'Object_Name__c, '
+ 'Filter__c, '
+ 'Type_Field__c, '
+ 'Drilldown_Id_Field__c, '
+ 'Tooltip_Id_Field__c, '
+ 'Tooltip_Object_Name__c, '
+ 'Fallback_Tooltip_Field__c '
+ 'FROM Timeline_Configuration__mdt '
+ 'WHERE Active__c = true AND '
+ 'Parent_Object__c =:parentConfigType'; //NOPMD
List<Timeline_Configuration__mdt> listOfTimelineConfigurations = Database.query( queryTimelineConfiguration ); //NOPMD
if ( listOfTimelineConfigurations.size() < 1 ) {
String errorMsg = 'No active records for parent entity \'' + parentObjectType + '\' have been found in \'Timeline_Configuration__mdt\'. Ask an administrator for help.';
throw new TimelineSetupException( '{"type": "Setup-Error", "message": "' + errorMsg + '"}' );
}
Map<String, TimelineRecord> mapOfTimelineConfigurationRecords = new Map<String, TimelineRecord>();
Map<String, String> mapOfFields = new Map<String, String>();
for ( Timeline_Configuration__mdt timelineConfigurationRecord : listOfTimelineConfigurations ) {
TimelineRecord timelineRecord = new TimelineRecord();
timelineRecord.relationshipName = timelineConfigurationRecord.Relationship_Name__c;
timelineRecord.icon = timelineConfigurationRecord.Icon__c;
timelineRecord.iconBackground = timelineConfigurationRecord.Icon_Background_Colour__c;
timelineRecord.detailField = timelineConfigurationRecord.Detail_Field__c;
timelineRecord.detailFieldToLabel = timelineConfigurationRecord.Detail_Field_to_Label__c;
timelineRecord.objectName = timelineConfigurationRecord.Object_Name__c;
timelineRecord.objectNameLabel = ((SObject)(Type.forName('Schema.'+ String.valueOf(timelineConfigurationRecord.Object_Name__c)).newInstance())).getSObjectType().getDescribe().getLabel();
timelineRecord.filter = timelineConfigurationRecord.Filter__c;
timelineRecord.type = timelineConfigurationRecord.Type_Field__c;
timelineRecord.positionDateField = timelineConfigurationRecord.Position_Date_Field__c;
timelineRecord.fallbackTooltipField = timelineConfigurationRecord.Fallback_Tooltip_Field__c;
timelineRecord.tooltipIdField = timelineConfigurationRecord.Tooltip_Id_Field__c;
timelineRecord.tooltipObject = timelineConfigurationRecord.Tooltip_Object_Name__c;
timelineRecord.drilldownIdField = timelineConfigurationRecord.Drilldown_Id_Field__c;
mapOfTimelineConfigurationRecords.put(timelineRecord.objectName + timelineRecord.relationshipName, timelineRecord);
mapOfFields.put(timelineRecord.detailField, timelineRecord.objectName);
mapOfFields.put(timelineRecord.positionDateField, timelineRecord.objectName);
mapOfFields.put(timelineRecord.fallbackTooltipField, timelineRecord.objectName);
mapOfFields.put(timelineRecord.tooltipIdField, timelineRecord.objectName);
mapOfFields.put(timelineRecord.drilldownIdField, timelineRecord.objectName);
mapOfFields.put(timelineRecord.type, timelineRecord.objectName);
}
Map<String, String> childObjects = getChildObjects(parentObjectType);
Map<String, FieldMetadata> fieldAttributes = getFieldMetadata(mapOfFields, parentObjectType);
String innerQuery = '';
for (String eachObject : mapOfTimelineConfigurationRecords.keySet()) {
TimelineRecord tcr = mapOfTimelineConfigurationRecords.get(eachObject);
if (childObjects.containsKey(eachObject)) {
String objName = String.valueOf(tcr.objectName);
String tooltipField = String.valueOf(tcr.fallbackTooltipField);
String tooltipIdField = String.valueOf(tcr.tooltipIdField);
String drilldownIdField = String.valueOf(tcr.tooltipIdField);
String typeField = String.valueOf(tcr.type);
String selectStatement = '(SELECT Id, '
+ (tcr.detailFieldToLabel ? ('toLabel(' + tcr.detailField + '), ') : tcr.detailField + ', ')
+ tcr.positionDateField + '';
if (typeField != null && typeField != '') {
selectStatement = selectStatement + ', '
+ tcr.type + '';
}
if ( objName == 'ContentDocumentLink' ) {
selectStatement = selectStatement + ', '
+ 'ContentDocumentId' + '';
}
if ( tooltipField != null && tooltipField != '' && tcr.detailField <> tcr.fallbackTooltipField) {
selectStatement = selectStatement + ', '
+ tcr.fallbackTooltipField + '';
}
if ( drilldownIdField != null && drilldownIdField != '' ) {
selectStatement = selectStatement + ', '
+ tcr.drilldownIdField + '';
}
if ( tooltipIdField != null && tooltipIdField != '' && tcr.drilldownIdField <> tcr.tooltipIdField) {
selectStatement = selectStatement + ', '
+ tcr.tooltipIdField + '';
}
String relationship = tcr.relationshipName;
if ( tcr.relationshipName.contains('Person') && !tcr.relationshipName.contains('__pr') ) {
relationship = tcr.relationshipName.substringAfter('Person');
}
innerQuery = innerQuery +
selectStatement +
+ ' FROM ' + relationship
+ ' WHERE ' + tcr.positionDateField + '>= LAST_N_MONTHS:' + earliestRange
+ ' AND ' + tcr.positionDateField + ' <= NEXT_N_MONTHS:' + latestRange;
if( tcr.filter != null && tcr.filter != ''){
innerQuery = innerQuery + ' AND ' + tcr.filter;
}
innerQuery = innerQuery + '),';
}
}
innerQuery = innerQuery.removeEnd(',');
String queryRecords = 'SELECT Id, '
+ innerQuery
+ ' FROM ' + parentObjectType
+ ' WHERE Id =:parentObjectId'; //NOPMD
List<SObject> listOfTimelineRecords = Database.query( queryRecords ); //NOPMD
List<Map<String, String>> listOfTimelineData = new List<Map<String, String>>();
for (SObject each : listOfTimelineRecords) {
for (String eachObj : mapOfTimelineConfigurationRecords.keySet()) {
if (each.getSObjects(childObjects.get(eachObj)) != null) {
for (SObject eachCh : (List<SObject>)each.getSObjects(childObjects.get(eachObj))) {
Map<String, String> mapData = new Map<String, String>();
TimelineRecord tr = mapOfTimelineConfigurationRecords.get(eachObj );
if ( tr != null ) {
String myId = eachCh.Id;
Map<String, String> detailValues = getFieldValues(tr.detailField, eachCh, fieldAttributes);
Map<String, String> positionValues = getFieldValues(tr.positionDateField, eachCh, fieldAttributes);
Map<String, String> fallbackValues = getFieldValues(tr.fallbackTooltipField, eachCh, fieldAttributes);
Map<String, String> tooltipIdValues = getFieldValues(tr.tooltipIdField, eachCh, fieldAttributes);
Map<String, String> drilldownIdValues = getFieldValues(tr.drilldownIdField, eachCh, fieldAttributes);
Map<String, String> typeValues = getFieldValues(tr.type, eachCh, fieldAttributes);
if ( tr.objectName == 'ContentDocumentLink') { //NOPMD
myId = String.valueOf(eachCh.get('ContentDocumentId'));
}
mapData.put('objectId', myId);
mapData.put('parentObject', parentObjectType);
if ( detailValues.get('value') == '' || detailValues.get('value') == null ) { //NOPMD
mapData.put('detailField', '[' + detailValues.get('label') +']');
}
else {
mapData.put('detailField', detailValues.get('value'));
}
mapData.put('detailFieldLabel', detailValues.get('label'));
mapData.put('positionDateField', tr.positionDateField);
mapData.put('positionDateValue', positionValues.get('value'));
mapData.put('objectName', tr.objectName);
mapData.put('objectNameLabel', tr.objectNameLabel);
mapData.put('fallbackTooltipField', fallbackValues.get('label'));
mapData.put('fallbackTooltipValue', fallbackValues.get('value'));
mapData.put('drilldownId', drilldownIdValues.get('value'));
mapData.put('tooltipId', tooltipIdValues.get('value'));
mapData.put('tooltipObject', tr.tooltipObject);
mapData.put('fallbackTooltipValue', fallbackValues.get('value'));
mapData.put('type', typeValues.get('value'));
mapData.put('icon', tr.icon);
mapData.put('iconBackground', tr.iconBackground);
listOfTimelineData.add(mapData);
}
}
}
}
}
return listOfTimelineData;
}
catch(Exception e) {
throw new AuraHandledException(e.getMessage());
}
}
private static Map<String, String> getChildObjects(String parentObject) {
Map<String, String> childRelatedObjects = new Map<String, String>();
List<Schema.ChildRelationship> objectRelationships = ((SObject)(Type.forName('Schema.'+ parentObject).newInstance())).getSObjectType().getDescribe().getChildRelationships();
for (Schema.ChildRelationship eachRelationship : objectRelationships) {
if (eachRelationship.getChildSObject().getDescribe().isAccessible()
&& !eachRelationship.getChildSObject().getDescribe().getLabel().contains('Histories')
&& eachRelationship.getRelationshipName() != null) {
childRelatedObjects.put(String.valueOf(eachRelationship.getChildSObject() + String.valueOf(eachRelationship.getRelationshipName())), String.valueOf(eachRelationship.getRelationshipName()));
}
}
return childRelatedObjects;
}
private static Map<String, FieldMetadata> getFieldMetadata(Map<String, String> fields, String baseObject) {
String fieldLabel;
Boolean fieldAccess;
Map<String, FieldMetadata> mapOfFieldMetadata = new Map<String, FieldMetadata>();
for (String field : fields.keySet() ) {
if ( field != null && field != '' ) {
String fieldObject = fields.get( field );
Boolean isDotNotationUsed = field.contains('.');
FieldMetadata fieldMetadata = new FieldMetadata();
try {
if ( isDotNotationUsed == true ) {
String splitObject = field.substringBefore('.');
String splitField = field.substringAfter('.');
Schema.DescribeSObjectResult describeParentSobjects = ((SObject)(Type.forName('Schema.'+ String.valueOf(splitObject)).newInstance())).getSObjectType().getDescribe();
fieldMetadata.label = String.valueOf( describeParentSobjects.fields.getMap().get(splitField).getDescribe().getLabel() );
fieldMetadata.canAccess = Boolean.valueOf(describeParentSobjects.fields.getMap().get(splitField).getDescribe().isAccessible() );
}
else {
Schema.DescribeSObjectResult describeSobjects = ((SObject)(Type.forName('Schema.'+ fieldObject).newInstance())).getSObjectType().getDescribe();
fieldMetadata.type = describeSobjects.fields.getMap().get(field).getDescribe().getType();
fieldMetadata.label = String.valueOf( describeSobjects.fields.getMap().get(field).getDescribe().getLabel() );
fieldMetadata.canAccess = Boolean.valueOf(describeSobjects.fields.getMap().get(field).getDescribe().isAccessible() );
}
}
catch(Exception e) {
String errorMsg = 'No such column \'' + field + '\' on entity \'' + fieldObject + '\'. If you are attempting to use a custom field, be sure to append the \'__c\' after the custom field name. ' + e.getMessage();
throw new TimelineSetupException( '{"type": "Setup-Error", "message": "' + errorMsg + '"}' );
}
mapOfFieldMetadata.put(field, fieldMetadata);
}
}
return mapOfFieldMetadata;
}
private static Map<String, String> getFieldValues(String field, SObject records, Map<String, FieldMetadata> fieldAttributes) {
Map<String, String> fieldDetails = new Map<String, String>();
String fieldValue = '';
String fieldLabel = '';
if ( field == null || field == '' ) {
fieldDetails.put('value' ,'');
fieldDetails.put('label', '');
return fieldDetails;
}
FieldMetadata fm = fieldAttributes.get(field );
Boolean isDotNotationUsed = field.contains('.');
fieldLabel = fm.label ;
if (fm.canAccess) {
if ( isDotNotationUsed == true ) {
String splitObject = field.substringBefore('.');
String splitField = field.substringAfter('.');
fieldValue = String.valueOf(records.getSObject(splitObject).get(splitField));
}
else {
if (fm.type == Schema.DisplayType.DATETIME && records.get(field) != null){
fieldValue = String.valueOf(((Datetime) records.get(field)).format('YYYY-MM-dd HH:mm:ss'));
} else {
fieldValue = String.valueOf(records.get(field));
}
}
}
if (fieldValue != null && fieldValue.length() > 255) {
fieldValue = fieldValue.substring(0,251) + '...';
}
fieldDetails.put('value', fieldValue);
fieldDetails.put('label', fieldLabel);
return fieldDetails;
}
private static Boolean isPersonAccount(String recordId)
{
if ( Account.SObjectType.getDescribe().hasSubtypes ) {
String queryPersonAccount = 'SELECT Id, IsPersonAccount FROM Account Where Id =:recordId';
SObject acc = Database.query( queryPersonAccount );
if ( acc.get('IsPersonAccount') == true ) {
return true;
}
}
return false;
}
private class TimelineRecord { //NOPMD
private String relationshipName;
private String parentObject;
private String detailField;
private Boolean detailFieldToLabel;
private String detailFieldLabel;
private String icon;
private String iconBackground;
private String positionDateField;
private String positionDateValue;
private String objectName;
private String objectNameLabel;
private String filter;
private String type;
private String tooltipIdField;
private String tooltipObject;
private String drilldownIdField;
private String fallbackTooltipField;
private String fallbackTooltipValue;
private String fallbackNameField;
private String fallbackNameValue;
private Id recordId;
}
private class FieldMetadata { //NOPMD
private Schema.DisplayType type;
private Boolean canAccess;
private String label;
}
private class TimelineSetupException extends Exception {}
}
Step 5 : Cree une autre LWC : timelineIllustration :
timelineIllustration.css
.illustration-img {
width: 75%;
height: 75%;
}
.slds-text-heading_medium {
padding-top: 10px;
}
.slds-text-body_regular {
padding-left: 2%;
padding-right: 2%;
}
timelineIlustration.html
<template>
<img class="illustration-img" src={illustration} alt="Illustration" />
<div class="slds-text-longform">
<h3 class="slds-text-heading_medium">{header}</h3>
<p class="slds-text-body_regular">{subHeader}</p>
</div>
</template>
timelineIllustration.js
import { LightningElement, api } from 'lwc';
import illustrations from '@salesforce/resourceUrl/timelineImages';
export default class timelineIllustration extends LightningElement {
@api header;
@api subHeader;
illustration;
illustrationType;
@api
get type() {
return this.illustrationType;
}
set type(value) {
this.illustrationType = value;
if (value != null && value !== '') {
this.illustration = illustrations + '/Illustrations/' + value + '.svg';
}
}
}
Step 6 : Ajouter les library Js : d3JS , momentJS et les icons dans les static resources et Ajouter les Labels aussi .
Link Github : https://github.com/deejay-hub/timeline-lwc
Comments