39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, jackdough
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/force-app/main/lwc-utils/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, james@sparkworks.io
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/force-app/main/lwc-utils/lwc/lwcUtilsLicense/lwcUtilsLicense.html:
--------------------------------------------------------------------------------
1 |
2 | BSD 3-Clause License
3 |
4 | Copyright (c) 2019, james@sparkworks.io
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions are met:
9 |
10 | * Redistributions of source code must retain the above copyright notice, this
11 | list of conditions and the following disclaimer.
12 |
13 | * Redistributions in binary form must reproduce the above copyright notice,
14 | this list of conditions and the following disclaimer in the documentation
15 | and/or other materials provided with the distribution.
16 |
17 | * Neither the name of the copyright holder nor the names of its
18 | contributors may be used to endorse or promote products derived from
19 | this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/force-app/main/lwc-utils/lwc/tableServiceUtils/tableServiceUtils.js:
--------------------------------------------------------------------------------
1 | const flattenObject = (propName, obj) => {
2 | let flatObject = {};
3 | for (let prop in obj) {
4 | if (prop) {
5 | //if this property is an object, we need to flatten again
6 | let propIsNumber = isNaN(propName);
7 | let preAppend = propIsNumber ? propName+'_' : '';
8 | if (typeof obj[prop] == 'object') {
9 | flatObject[preAppend+prop] = {...flatObject, ...flattenObject(preAppend+prop,obj[prop])};
10 | } else {
11 | flatObject[preAppend+prop] = obj[prop];
12 | }
13 | }
14 | }
15 | return flatObject;
16 | }
17 |
18 | const flattenQueryResult = (listOfObjects) => {
19 | let finalArr = [];
20 | for (let i=0; i {
40 | let dataClone = JSON.parse(JSON.stringify(flatData));
41 | for (let row of dataClone) {
42 | Object.keys(row).forEach(key => {
43 | if (key.endsWith('Id')) {
44 | row[key.replace('Id','Link')] = '/'+row[key];
45 | }
46 | });
47 | }
48 | return dataClone;
49 | }
50 |
51 | export {
52 | flattenQueryResult,
53 | applyLinks
54 | }
--------------------------------------------------------------------------------
/force-app/main/default/flexipages/tst.flexipage-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | region1
5 | Region
6 |
7 |
8 | region2
9 | Region
10 |
11 |
12 |
13 |
14 | fields
15 | Name,Phone,Website
16 |
17 |
18 | filter
19 |
20 |
21 | hideCheckboxColumn
22 | false
23 |
24 |
25 | recordsPerBatch
26 | 50
27 |
28 |
29 | sObject
30 | Account
31 |
32 |
33 | sortedBy
34 | Name
35 |
36 |
37 | sortedDirection
38 | asc
39 |
40 | relatedList
41 |
42 | region3
43 | Region
44 |
45 | tst
46 |
47 | flexipage:appHomeTemplateHeaderTwoColumnsLeftSidebar
48 |
49 | AppPage
50 |
51 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/relatedList/relatedList.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {title}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
36 |
37 |
38 |
39 |
40 |
41 |
48 |
49 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/datatablePicklistField/datatablePicklistField.js:
--------------------------------------------------------------------------------
1 | import { LightningElement, api, track } from 'lwc';
2 |
3 | // import displayTemplate from './display.html';
4 | // import editTemplate from './edit.html'
5 | export default class DatatablePicklistField extends LightningElement {
6 | @api rowKeyValue;
7 | @api colKeyValue;
8 | @api
9 | get value() {
10 | return this._value;
11 | }
12 | set value(val) {
13 | this._value = val;
14 | this._editedValue = val;
15 | }
16 | @api
17 | get options() {
18 | return this._options;
19 | }
20 | set options(value) {
21 | if (!Array.isArray(value)) {
22 | throw new Error('Picklist options must be an array');
23 | }
24 | this._options = value;
25 | this.valueToLabelMap = value.reduce((acc,opt) => {
26 | acc[opt.value] = opt.label;
27 | return acc;
28 | }, {});
29 | }
30 | @api editable;
31 |
32 | @track editing;
33 | @track _options;
34 | @track _value;
35 | _editedValue;
36 | valueToLabelMap={};
37 | editRendered;
38 |
39 | renderedCallback() {
40 | const combobox = this.template.querySelector('lightning-combobox');
41 | if (!combobox) {
42 | this.editRendered = false;
43 | } else if (!this.editRendered ) {
44 | combobox.focus();
45 | // combobox.click();
46 | this.editRendered = true;
47 | }
48 | }
49 | handleFocusout() {
50 | if (this._editedValue !== this._value) {
51 | this.dispatchEvent(new CustomEvent('inlineedit', {
52 | detail: {
53 | value: this._editedValue,
54 | rowKeyValue: this.rowKeyValue,
55 | colKeyValue: this.colKeyValue
56 | },
57 | bubbles: true,
58 | composed: true
59 | }));
60 | }
61 | this.editing=false;
62 | }
63 |
64 | handleEdit() {
65 | this.editing=true;
66 |
67 | }
68 |
69 | handleChange(event) {
70 | this._editedValue = event.detail.value;
71 | this.handleFocusout();
72 | // this.template.focus();
73 | }
74 |
75 | get editClass() {
76 | return this.editable ? 'editable ' : '';
77 | }
78 |
79 | get displayValue() {
80 | return this.valueToLabelMap[this.value] || this.value;
81 | }
82 |
83 | }
--------------------------------------------------------------------------------
/force-app/main/default/flexipages/contact.flexipage-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | header
5 | Region
6 |
7 |
8 |
9 |
10 | childRecordField
11 | AccountId
12 |
13 |
14 | fields
15 | Name
16 |
17 |
18 | filter
19 |
20 |
21 | hideCheckboxColumn
22 | false
23 |
24 |
25 | parentRecordField
26 | AccountId
27 |
28 |
29 | recordsPerBatch
30 | 50
31 |
32 |
33 | sObject
34 | Contact
35 |
36 |
37 | showSoql
38 | true
39 |
40 |
41 | sortedBy
42 | Name
43 |
44 |
45 | sortedDirection
46 | asc
47 |
48 | relatedList
49 |
50 | main
51 | Region
52 |
53 | contact
54 | Contact
55 |
56 | flexipage:recordHomeSimpleViewTemplate
57 |
58 | RecordPage
59 |
60 |
--------------------------------------------------------------------------------
/force-app/main/default/flexipages/test.flexipage-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | header
5 | Region
6 |
7 |
8 |
9 |
10 | childRecordField
11 | AccountId
12 |
13 |
14 | fields
15 | Name,Phone,Website
16 |
17 |
18 | filter
19 |
20 |
21 | editable
22 | true
23 |
24 |
25 | hideCheckboxColumn
26 | false
27 |
28 |
29 | parentRecordField
30 | Id
31 |
32 |
33 | recordsPerBatch
34 | 50
35 |
36 |
37 | sObject
38 | Account
39 |
40 |
41 | sortedBy
42 | Name
43 |
44 |
45 | sortedDirection
46 | asc
47 |
48 | relatedList
49 |
50 | main
51 | Region
52 |
53 | test
54 | Account
55 |
56 | flexipage:recordHomeSimpleViewTemplate
57 |
58 | RecordPage
59 |
60 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | lint:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: bahmutov/npm-install@v1
14 | - name: Run ESLint
15 | run: |
16 | yarn
17 | yarn lint:ci
18 |
19 | jest:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v2
23 | - uses: bahmutov/npm-install@v1
24 | - name: Run Jest tests
25 | run: |
26 | yarn
27 | yarn test:ci
28 | - name: Upload code coverage
29 | uses: codecov/codecov-action@v1
30 | with:
31 | token: ${{ secrets.CODECOV_TOKEN }} #required
32 | name: js
33 |
34 | apex_tests:
35 | runs-on: ubuntu-latest
36 | # env:
37 | # scratch_username: ${{ secrets.SFDX_USERNAME }}.listview.ci
38 |
39 | steps:
40 | - uses: actions/checkout@v2
41 | - name: Install Salesforce CLI
42 | run: |
43 | wget https://developer.salesforce.com/media/salesforce-cli/sfdx-linux-amd64.tar.xz
44 | mkdir sfdx-cli
45 | tar xJf sfdx-linux-amd64.tar.xz -C sfdx-cli --strip-components 1
46 | ./sfdx-cli/install
47 | - name: Populate auth file with SFDX_URL secret
48 | run: echo "${{ secrets.SFDX_JWT_KEY }}" > ./server.key
49 | - name: Authenticate Dev Hub with JWT
50 | run: |
51 | sfdx force:auth:jwt:grant \
52 | --clientid ${{ secrets.SFDX_CONSUMER_KEY }} \
53 | --jwtkeyfile ./server.key \
54 | --username ${{ secrets.SFDX_USERNAME }} \
55 | --setdefaultdevhubusername
56 | - name: Create a new scratch org
57 | run: |
58 | sfdx force:org:create \
59 | --definitionfile=config/project-scratch-def.json \
60 | --setdefaultusername \
61 | --setalias=scratch-org
62 | - name: Push source
63 | run: |
64 | sfdx force:source:push \
65 | --targetusername=scratch-org \
66 | --forceoverwrite
67 | - name: Run Apex tests
68 | run: |
69 | sfdx force:apex:test:run \
70 | --targetusername=scratch-org \
71 | --codecoverage \
72 | --synchronous \
73 | --resultformat=human \
74 | --outputdir=coverage
75 | - name: Delete scratch org
76 | if: always()
77 | run: |
78 | sfdx force:org:delete \
79 | --targetusername=scratch-org \
80 | --noprompt
81 | - name: Upload code coverage
82 | uses: codecov/codecov-action@v1
83 | with:
84 | token: ${{ secrets.CODECOV_TOKEN }} #required
85 | name: apex
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LWC Listview
2 |
3 | [](https://codecov.io/gh/shliachtx/lwc-listview)
4 |
5 |
6 | _No warranty is provided, express or implied_
7 |
8 | [Install unlocked package](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008aqeqAAA) version 0.13.5
9 |
10 | ## Release Notes
11 | ### 0.13.5
12 | - Add the ability to show an icon in the header
13 | - Display errors on datatable
14 | - Block sorting on fields that are not sortable
15 | - Fix issue where newly inserted records would show duplicates if the table was refreshed immediately after creating the new record.
16 | - Fix issues with picklist field. (thanks to @tsalb for his help with this)
17 | ### 0.11.0
18 | - Add `editFieldName` option to datatable - allows for field fronting in edit mode
19 | ### 0.10.0
20 | - Change live data updates to use PushTopic
21 | ### 0.8.0
22 | - Add support for Change Data Capture.
23 | ### 0.7.0
24 | - Fix problem with infinite loading sometimes not working.
25 | ### 0.6.0
26 | - Picklist fields dropdown are auto populated, options will now override the default. Does not support RecordType dependent picklists.
27 | ### 0.4.0
28 | - Picklist fields! The options need to be manually set on the field JSON using the `options` property. Accepts an array of strings or `{label, value}` objects
29 | ### 0.3.0
30 | - Allow custom label on datatable columns
31 | - Fix issue in related list that prevented using a filter string if there was no parent-child relationship set.
32 | ### 0.2.0
33 | - Add option to create a record from a related list
34 | ### 0.1.0
35 | - Add option to edit related list inline
36 |
37 |
38 | ## Dev, Build and Test
39 |
40 | To setup, clone the repository locally, and from the home directory run `$ yarn`.
41 |
42 | To test lwc components locally run `$ yarn:test` with sfdx installed.
43 |
44 | To deploy authorize a dev hub in sfdx and run `$ sfdx force:org:create -f config/project-scratch-def.json -a MyScratchOrg` followed by `$ sfdx force:source:push -u MyScratchOrg`
45 |
46 |
47 | ## Resources
48 |
49 | ## Description of Files and Directories
50 |
51 | ### [datatable](force-app/main/default/lwc/datatable)
52 | Takes as input an sObject and an array of fields and populates a datatable with records from the database.
53 |
54 | Note: Streaming update support utilizes the PushTopic feature, which has a maximum of 50 PushTopic records per org. The datatable uses one for each object type that has live updates enabled. They can be deleted or deactivated if necessary - use `SELECT Id, IsActive FROM PushTopic WHERE Name LIKE 'easydt__%` to retrieve them via SOQL.
55 |
56 | ### [Custom Related List](force-app/main/default/lwc/relatedList)
57 | Related list for use on lightning app and record pages. Choose object, fields, etc.
58 |
59 | Fields accepts a comma separated list of fields, or a JSON list with field information. For more documentation see [datatable](force-app/main/default/lwc/datatable)
60 |
61 | 
62 |
63 | ## Issues
64 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/datatable/__tests__/data/wireTableCache.json:
--------------------------------------------------------------------------------
1 | {
2 | "tableData": [
3 | {
4 | "Id": "0063F00000IupIYQAZ",
5 | "Name": "United Oil Emergency Generators",
6 | "StageName": "option 2",
7 | "CloseDate": "2019-10-22"
8 | },
9 | {
10 | "Id": "0063F00000IupIQQAZ",
11 | "Name": "United Oil Installations",
12 | "StageName": "Closed Won",
13 | "CloseDate": "2019-10-28"
14 | },
15 | {
16 | "Id": "0063F00000IupINQAZ",
17 | "Name": "United Oil Installations",
18 | "StageName": "Negotiation/Review",
19 | "CloseDate": "2019-10-29"
20 | },
21 | {
22 | "Id": "0063F00000IupIAQAZ",
23 | "Name": "United Oil Office Portable Generators",
24 | "StageName": "Negotiation/Review",
25 | "CloseDate": "2019-11-02"
26 | },
27 | {
28 | "Id": "0063F00000IupIXQAZ",
29 | "Name": "United Oil Installations",
30 | "StageName": "Closed Won",
31 | "CloseDate": "2019-11-09"
32 | },
33 | {
34 | "Id": "0063F00000IupIcQAJ",
35 | "Name": "United Oil Plant Standby Generators",
36 | "StageName": "Needs Analysis",
37 | "CloseDate": "2019-11-26"
38 | },
39 | {
40 | "Id": "0063F00000IupISQAZ",
41 | "Name": "United Oil Refinery Generators",
42 | "StageName": "Closed Won",
43 | "CloseDate": "2019-12-10"
44 | },
45 | {
46 | "Id": "0063F00000IupIEQAZ",
47 | "Name": "United Oil Refinery Generators",
48 | "StageName": "Proposal/Price Quote",
49 | "CloseDate": "2019-12-17"
50 | },
51 | {
52 | "Id": "0063F00000IupIFQAZ",
53 | "Name": "United Oil SLA",
54 | "StageName": "Closed Won",
55 | "CloseDate": "2019-12-24"
56 | },
57 | {
58 | "Id": "0063F00000IupIaQAJ",
59 | "Name": "United Oil Standby Generators",
60 | "StageName": "Closed Won",
61 | "CloseDate": "2019-12-25"
62 | }
63 | ],
64 | "tableColumns": [
65 | {
66 | "label": "Name",
67 | "type": "url",
68 | "fieldName": "Link",
69 | "typeAttributes": {
70 | "label": { "fieldName": "Name" },
71 | "target": "_parent"
72 | }
73 | },
74 | {
75 | "label": "Stage",
76 | "type": "picklist",
77 | "fieldName": "StageName",
78 | "options": [
79 | { "label": "Prospecting", "value": "Prospecting" },
80 | { "label": "Qualification", "value": "Qualification" },
81 | { "label": "Needs Analysis", "value": "Needs Analysis" },
82 | { "label": "Value Proposition", "value": "Value Proposition" },
83 | { "label": "Id. Decision Makers", "value": "Id. Decision Makers" },
84 | { "label": "Perception Analysis", "value": "Perception Analysis" },
85 | { "label": "Proposal/Price Quote", "value": "Proposal/Price Quote" },
86 | { "label": "Negotiation/Review", "value": "Negotiation/Review" },
87 | { "label": "Closed Won", "value": "Closed Won" },
88 | { "label": "Closed Lost", "value": "Closed Lost" }
89 | ]
90 | },
91 | { "label": "Close Date", "type": "date-local", "fieldName": "CloseDate" }
92 | ],
93 | "recordCount": 10
94 | }
95 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/relatedList/relatedList.js:
--------------------------------------------------------------------------------
1 | import { LightningElement, api, track, wire } from "lwc";
2 | import { getRecord, getFieldValue } from "lightning/uiRecordApi";
3 | import { getObjectInfo } from 'lightning/uiObjectInfoApi';
4 |
5 |
6 | export default class RelatedList extends LightningElement {
7 | @api showAddButton;
8 | @api modalEdit;
9 | _title;
10 | @api
11 | get title() {
12 | return this._title || (this.objectInfo && this.objectInfo.labelPlural) || this.sObject;
13 | }
14 | set title(value) {
15 | this._title = value;
16 | }
17 | @api recordId;
18 | @api objectApiName
19 | @api sObject;
20 | @api fields;
21 | @api sortedBy;
22 | @api sortedDirection;
23 | _filter;
24 | @api
25 | get filter() {
26 | if (this._filter && this.parentRelationship) {
27 | return this.parentRelationship + ' AND ' + this._filter;
28 | }
29 | return this._filter || this.parentRelationship;
30 | }
31 | set filter(value) {
32 | this._filter = value;
33 | }
34 | @api hideCheckboxColumn;
35 | @api enableInfiniteLoading;
36 | @api recordsPerBatch=50;
37 | @api initialRecords;
38 | @api showSoql;
39 | @api parentRecordField;
40 | @api childRecordField;
41 | @api editable;
42 | @api height;
43 | @api enableLiveUpdates;
44 | @api iconName;
45 |
46 | _parentRecordField;
47 | _childRecordField;
48 | @track objectInfo;
49 |
50 | @wire(getObjectInfo, { objectApiName: '$sObject' })
51 | wiredObjectInfo({ error, data }) {
52 | if (data) {
53 | this.objectInfo = data;
54 | } else if (error) {
55 | this.error(error.statusText + ': ' + error.body.message);
56 | }
57 | }
58 |
59 |
60 | @wire(getRecord, { recordId: "$recordId", fields: "$fullParentRecordField" })
61 | parentRecord;
62 |
63 | get fullParentRecordField() {
64 | return this.objectApiName + "." + this.parentRecordField;
65 | }
66 |
67 | get parentRecordId() {
68 | if (!this.parentRecordField) {
69 | return undefined;
70 | } else if (this.parentRecord && this.parentRecord.data) {
71 | return getFieldValue(this.parentRecord.data, this.fullParentRecordField);
72 | }
73 | return "";
74 | }
75 |
76 | get parentRelationship() {
77 | if (this.childRecordField && typeof this.parentRecordId !== undefined) {
78 | return `${this.childRecordField}='${this.parentRecordId}'`;
79 | }
80 | return "";
81 | }
82 |
83 | // used to render datatable. Important because otherwise, initial query can be very inefficient and cause slow load times
84 | get ready() {
85 | return !!this.parentRecordId || !this.parentRecordField;
86 | }
87 |
88 | refresh() {
89 | this.template.querySelector('c-datatable').refresh();
90 | }
91 | get customStyle() {
92 | if (this.height) {
93 | return 'height:'+this.height+'px';
94 | }
95 | return '';
96 | }
97 |
98 | get addRecordTitle() {
99 | return this.objectInfo ? 'New ' + this.objectInfo.label : 'Loading...';
100 | }
101 |
102 | createNew() {
103 | this.template.querySelector('c-modal').open();
104 | }
105 | handleCancel() {
106 | this.template.querySelector('c-modal').close();
107 | }
108 | handleSuccess() {
109 | this.template.querySelector('c-datatable').refresh();
110 | this.template.querySelector('c-modal').close();
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/force-app/main/default/lwc/datatable/README.md:
--------------------------------------------------------------------------------
1 | # datatable
2 | ## Description
3 | Takes as input an sObject and an array of fields and populates a datatable with records from the database.
4 |
5 | ### Fields:
6 |
7 | {
8 | fieldName (required),,
9 | editFieldName (name of field to save when editing),
10 | label (defaults to field label),
11 | searchable (defaults to true on text fields),
12 | sortable (defaults to true),
13 | visible (defaults to true),
14 | editable (defaults to table setting),
15 | options (array of options for picklist - Array of strings or objects with `label` and `value`)
16 | }
17 |
18 | Notes one `editFieldName`: This is designed for use with a calculated (formula) field that fronts field in Salesforce. Use carefully! It may not be obvious to users that they are editing a different field. Additionally, all the metadata comes from the original field - not the edit field! So make sure that they are the same type.
19 |
20 |
21 | #### Example:
22 |
23 | [
24 | {
25 | "fieldName": "Name"
26 | },
27 | {
28 | "fieldName": "Phone",
29 | "sortable": false,
30 | "editable": true
31 | },
32 | {
33 | "fieldName": "Account.Website",
34 | "label": "Website"
35 | }
36 | ]
37 |
38 | ### Row Actions:
39 | Contains a label and a callback that does something with the selected row. The callback should take an input of a row object and return a promise. If the return value is `false` the row will be deleted from the datatable, otherwise the row will be updated with return value of the promise (if there is one).
40 |
41 | {
42 | label,
43 | callback
44 | }
45 |
46 |
47 | ## Properties
48 | Name | Type |Read only | Required | Description | Default value
49 | ---|---|---|---|---|---
50 | `s-object`|string||✔| name of Salesforce object
51 | `fields`|array||✔|fields to display. Optionally a comma separated list of fields
52 | `sorted-by`|string||✔|field to sort table by
53 | `sorted-direction`|string|||`asc` or `desc`|`asc`
54 | `editable`|boolean|||make the entire table editable (by default). this can also be set on the field level|`false`
55 | `filter`|string|||string to filter by - excluding the where clause. e.g. `Name='Bob' AND Total_Donations__c > 1000`
56 | `search`|string|||text to search in all searchable text fields
57 | `row-actions`|array or function|||array of row actions to display on each row. optionally a function which takes as input the rwo of the datable and returns an array of row actions
58 | `hide-checkbox-column`|boolean|||hide checkboxes from table (disable row selection)
59 | `enable-infinite-loading`|boolean|||automatically load more records when user reaches the end of the datatable|`false`
60 | `records-per-batch`|integer|||number of records to load when the end of the datable is reached|`50`
61 | `initial-records`|integer|||number of records to load initially|`this.recordsPerBatch`
62 | `enableLiveUpdates`|boolean|||update records using PushTopic|`false`
63 | `selected-rows`|array|✔||array of selected IDs from datatable
64 | `query`|string|✔||generated query string used to retrieve data
65 | `record-count`|integer|✔||total number of records returned by current query
66 |
67 | ## Methods
68 | Name | Parameters | Return | Description
69 | ---|---|---|---
70 | `refresh`|||refresh the data in the datatable using the current fields and filters
71 | `clearSelection`|||clear all selected rows
72 |
73 | ## Events
74 | Name|Detail|Bubbles
75 | ---|---|---
76 | `rowselection`| `{ selectedRows }`
77 | `loaddata`| `{ recordCount, sortedDirection, sortedBy }`
--------------------------------------------------------------------------------
/force-app/main/default/lwc/datatable/datatableUtils.js:
--------------------------------------------------------------------------------
1 | const addFieldMetadata = (columns, fieldOptions) => {
2 | return JSON.parse(JSON.stringify(columns))
3 | .map(col => {
4 | let fieldName = col.fieldName;
5 | if (fieldName.endsWith('Link')) { // special case for salesforce relationship fields (this will not work for custom relationships)
6 | fieldName = fieldName.replace('_Link', '.Name');
7 | fieldName = fieldName.replace('Link', 'Name')
8 | }
9 | let field = fieldOptions.find(f => (f.fieldName === fieldName));
10 | if (field) { // copy values from fields list to columns list
11 | // col.sortable = field.sortable;
12 | // col.visible = field.visible;
13 | // col.editable = field.editable;
14 | // col.label = field.label || col.label;
15 | Object.assign(col,field);
16 | col.typeAttributes = col.typeAttributes || {};
17 | col.typeAttributes.editable = field.editable;
18 | col.typeAttributes.options = field.options || col.options || [];
19 | }
20 | return col;
21 | })
22 | .filter(col => col.visible);
23 | };
24 |
25 | const addObjectInfo = (columns, objectInfo) => {
26 | return columns.map(col => {
27 | let fieldName = col.fieldName;
28 | if (!fieldName) {
29 | return col;
30 | }
31 | if (fieldName.endsWith('Link')) { // special case for salesforce relationship fields (this will not work for custom relationships)
32 | fieldName = fieldName.replace('_Link', '.Name');
33 | fieldName = fieldName.replace('Link', 'Name')
34 | }
35 | let fieldInfo = objectInfo && objectInfo.fields && objectInfo.fields[fieldName];
36 | if (fieldInfo) {
37 | col.sortable = fieldInfo.sortable && col.sortable; // field is not sortable if
38 | }
39 | return col;
40 | });
41 | }
42 |
43 | const addRowActions = (columns, rowActions) => {
44 | if (rowActions && rowActions.length || typeof rowActions === 'function') {
45 | columns.push({
46 | type: 'action',
47 | typeAttributes: {
48 | rowActions: rowActions
49 | }
50 | });
51 | }
52 | return columns;
53 | };
54 |
55 | const getNumberOfRecordsToLoad = (numberOfLoadedRecords, recordsPerBatch, maxRecords) => {
56 | if ((recordsPerBatch + numberOfLoadedRecords) <= maxRecords) {
57 | return recordsPerBatch;
58 | }
59 | return maxRecords - numberOfLoadedRecords;
60 | }
61 |
62 | const createFieldArrayFromString = (value) => {
63 | let fields;
64 | if (value.substring(0,1)==='[') {
65 | fields = JSON.parse(value);
66 | } else {
67 | fields = value.split(',')
68 | .map(field => {
69 | return {
70 | fieldName: field.trim()
71 | };
72 | });
73 | }
74 | return fields;
75 | }
76 |
77 | const addDefaultFieldValues = (fields, editable) => {
78 | return fields.map(field => {
79 | if (!field.fieldName) throw new Error('Field must have a valid `fieldName` property');
80 |
81 | if (typeof field.visible === 'undefined')
82 | field.visible = true; // default true
83 | else
84 | field.visible = !!field.visible; // convert to boolean
85 |
86 | if (typeof field.sortable === 'undefined')
87 | field.sortable = true; // default true
88 | else
89 | field.sortable = !!field.sortable; // convert to boolean
90 |
91 | if (typeof field.editable === 'undefined') {
92 | field.editable = (
93 | !!field.editFieldName || // if editFieldName is set, assume field is editable
94 | field.fieldName === 'StageName' ||
95 | !field.fieldName.endsWith('Name') // &&
96 | // !field.fieldName.endsWith('Link') &&
97 | // !field.fieldName.endsWith('Id')
98 | ) && editable; // default to global setting
99 | } else {
100 | field.editable = !!field.editable; // convert to boolean
101 | }
102 |
103 | if (field.options &&
104 | Array.isArray(field.options) &&
105 | field.options.every(opt=> typeof opt === 'string')) {
106 |
107 | field.options = field.options.map(opt => {return {label: opt, value: opt}});
108 |
109 | }
110 | return field;
111 | });
112 | }
113 |
114 | export {
115 | addFieldMetadata,
116 | addObjectInfo,
117 | addRowActions,
118 | getNumberOfRecordsToLoad,
119 | createFieldArrayFromString,
120 | addDefaultFieldValues
121 | };
--------------------------------------------------------------------------------
/force-app/main/default/lwc/relatedList/relatedList.js-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 47.0
4 | true
5 | Custom Related List
6 |
7 | lightning__AppPage
8 | lightning__HomePage
9 | lightning__RecordPage
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/force-app/main/lwc-utils/classes/DataTableServiceTests.cls:
--------------------------------------------------------------------------------
1 | @isTest
2 | private class DataTableServiceTests {
3 |
4 | @isTest
5 | static void test_missing_query() {
6 | Map tableServiceRequest = new Map();
7 | Map tableServiceResponse = new Map();
8 | String errorMessage;
9 |
10 | Test.startTest();
11 | try {
12 | tableServiceResponse = DataTableService.getTableCache(tableServiceRequest);
13 | } catch (Exception e) {
14 | errorMessage = e.getMessage();
15 | }
16 | Test.stopTest();
17 |
18 | System.debug('test_missing_query errorMessage is: '+errorMessage);
19 | System.assert(tableServiceResponse.isEmpty());
20 | System.assert(!tableServiceResponse.containsKey(DataTableService.TABLE_DATA_KEY));
21 | System.assert(!tableServiceResponse.containsKey(DataTableService.TABLE_COLUMNS_KEY));
22 | System.assert(String.isNotEmpty(errorMessage));
23 | }
24 |
25 | @isTest
26 | static void test_query_no_where_filter() {
27 | Map tableServiceRequest = new Map();
28 | Map tableServiceResponse = new Map();
29 | String queryString = 'SELECT Id, Name, Email FROM User';
30 |
31 | tableServiceRequest.put(DataTableService.QUERY_STRING_KEY, queryString);
32 |
33 | Test.startTest();
34 | tableServiceResponse = DataTableService.getTableCache(tableServiceRequest);
35 | Test.stopTest();
36 |
37 | System.assert(!tableServiceResponse.isEmpty());
38 | System.assert(tableServiceResponse.containsKey(DataTableService.TABLE_DATA_KEY));
39 | System.assert(tableServiceResponse.containsKey(DataTableService.TABLE_COLUMNS_KEY));
40 |
41 | List users = (List) tableServiceResponse.get(DataTableService.TABLE_DATA_KEY);
42 | System.assert(!users.isEmpty());
43 |
44 | List