Skip to content

Commit

Permalink
Lightning record picker multi value (#893)
Browse files Browse the repository at this point in the history
* feat: creating record-picker-multi-value scenario + using record-picker component

* feat: adding pill-container to displayed selected records

* feat: handling pill-container itemremove event

* feat: adding logic to filters fetched records based on the one that already has been selected

* feat: creating test for record-picker-multi-value

* test: should clear the input when a selection is made

* feat: adding test for records filtering

* refacto: clean-up tests and comments

* refacto: add state

* refacto: simplify code (remove state)

* chore: add comments + error and view source panel

* feat: adding lightning-card to the recordPickerMultiValue cmp

* feat: adding recordPickerMultiValue cmp to recordPicker flexipage

* fix: display icon in pill items

* fix: Reset recordId to ensure the wire can be called a second time the same recordId

* change to gql wire

* fix gql

* chore: fix typo

* pr: adjust view source footer

---------

Co-authored-by: skempf <skempf@salesforce.com>
  • Loading branch information
arosenbach and SimonKempf authored Jan 12, 2024
1 parent 33b6697 commit 4f57051
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<type>Region</type>
</flexiPageRegions>
<flexiPageRegions>
<itemInstances>
<componentInstance>
<componentName>recordPickerMultiValue</componentName>
<identifier>c_recordPickerMultiValue</identifier>
</componentInstance>
</itemInstances>
<name>region4</name>
<type>Region</type>
</flexiPageRegions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,13 @@ describe('recordPickerHello', () => {
detail: { recordId: '003Z70000016iOUIAY' }
})
);
// Emit data from @wire
// Emit data from @wire and wait for any asynchronous DOM updates
graphql.emit(mockGraphQL);

// Wait for any asynchronous DOM updates
await flushPromises();

const selectedRecordDetails = element.shadowRoot.querySelector(
'.selectedRecordDetails'
);
await flushPromises();

expect(selectedRecordDetails).toBeTruthy();
});
Expand All @@ -96,25 +94,21 @@ describe('recordPickerHello', () => {
detail: { recordId: null }
})
);
// Emit data from @wire
// Emit data from @wire and wait for any asynchronous DOM updates
graphql.emit(mockGraphQLEmptyResults);

// Wait for any asynchronous DOM updates
await flushPromises();

const selectedRecordDetails = element.shadowRoot.querySelector(
'.selectedRecordDetails'
);
await flushPromises();

expect(selectedRecordDetails).toBeFalsy();
});

describe('graphql @wire error', () => {
it('shows error panel element', async () => {
// Emit error from @wire
// Emit error from @wire and wait for any asynchronous DOM updates
graphql.emitErrors(['an error']);

// Wait for any asynchronous DOM updates
await flushPromises();

// Check for error panel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"uiapi": {
"query": {
"Contact": {
"edges": [
{
"node": {
"Id": "005xx000016QpSqAAK",
"Name": {
"value": "Amy Taylor"
},
"Title": {
"value": "VP of Engineering"
},
"Phone": {
"value": "4152568563"
},
"Email": {
"value": "amy@demo.net"
},
"Picture__c": {
"value": "https://s3-us-west-2.amazonaws.com/dev-or-devrl-s3-bucket/sample-apps/people/amy_taylor.jpg"
}
}
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { createElement } from 'lwc';
import { graphql } from 'lightning/uiGraphQLApi';
import RecordPickerMultiValue from 'c/recordPickerMultiValue';

// Mock realistic data
const mockGraphQL = require('./data/graphqlContactResult.json');

// Helper function to wait until the microtask queue is empty. This is needed for promise
// timing when calling imperative Apex.
async function flushPromises() {
return Promise.resolve();
}

describe('recordPickerMultiValue', () => {
let element;
beforeEach(() => {
// Create component
element = createElement('c-record-picker-multi-value', {
is: RecordPickerMultiValue
});
element.objectApiName = 'Contact';
element.label = 'Contact';
document.body.appendChild(element);
});

afterEach(() => {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
// Prevent data saved on mocks from leaking between tests
jest.clearAllMocks();
});

it('is accessible', async () => {
expect(element).toBeAccessible();
});

it('should display the selected record in the pill container', async () => {
// Set selected record
const recordPickerElement = element.shadowRoot.querySelector(
'lightning-record-picker'
);
recordPickerElement.value = '005xx000001X83aAAC';
recordPickerElement.dispatchEvent(
new CustomEvent('change', {
detail: { recordId: '005xx000001X83aAAC' }
})
);
await flushPromises();

// Emit data from @wire
graphql.emit(mockGraphQL);
await flushPromises();

const pillContainer = element.shadowRoot.querySelector(
'lightning-pill-container'
);

expect(pillContainer.items).toEqual([
{
name: '005xx000016QpSqAAK',
label: 'Amy Taylor',
iconName: 'standard:contact',
type: 'icon'
}
]);
});

it('should clear the input when a selection is made', async () => {
const recordPickerElement = element.shadowRoot.querySelector(
'lightning-record-picker'
);

// Spy on recordPickerElement.clearSelection()
const clearSelection = jest.spyOn(
recordPickerElement,
'clearSelection'
);

// Simulate a record selection in the record picker
recordPickerElement.value = '005xx000001X83aAAC';
recordPickerElement.dispatchEvent(
new CustomEvent('change', {
detail: { recordId: '005xx000001X83aAAC' }
})
);
await flushPromises();

// Emit data from @wire
graphql.emit(mockGraphQL);
await flushPromises();

expect(clearSelection).toHaveBeenCalled();
});

it('should filter out a record from the suggestions when it has already been selected', async () => {
// Set selected record
const recordPickerElement = element.shadowRoot.querySelector(
'lightning-record-picker'
);
recordPickerElement.value = '005xx000016QpSqAAK';
recordPickerElement.dispatchEvent(
new CustomEvent('change', {
detail: { recordId: '005xx000016QpSqAAK' }
})
);
await flushPromises();

// Emit data from @wire
graphql.emit(mockGraphQL);
await flushPromises();

expect(recordPickerElement.filter.criteria).toEqual(
expect.arrayContaining([
{
fieldPath: 'Id',
operator: 'nin',
value: ['005xx000016QpSqAAK']
}
])
);
});

it('should remove the corresponding pill when selected record is removed', async () => {
// Set selected record
const recordPickerElement = element.shadowRoot.querySelector(
'lightning-record-picker'
);
recordPickerElement.value = '005xx000001X83aAAC';
recordPickerElement.dispatchEvent(
new CustomEvent('change', {
detail: { recordId: '005xx000001X83aAAC' }
})
);
// Emit data from @wire
graphql.emit(mockGraphQL);
await flushPromises();

// Simulate a selection removal
const pillContainer = element.shadowRoot.querySelector(
'lightning-pill-container'
);
pillContainer.dispatchEvent(
new CustomEvent('itemremove', {
detail: {
item: {
name: '005xx000016QpSqAAK',
label: 'Amy Taylor',
iconName: 'standard:contact'
}
}
})
);
await flushPromises();

// The pill item has been removed
expect(pillContainer.items).toEqual([]);
});

it('should remove a record from filter when it has been removed from selection', async () => {
// Set selected record
const recordPickerElement = element.shadowRoot.querySelector(
'lightning-record-picker'
);
recordPickerElement.value = '005xx000001X83aAAC';
recordPickerElement.dispatchEvent(
new CustomEvent('change', {
detail: { recordId: '005xx000001X83aAAC' }
})
);
await flushPromises();

// Emit data from @wire
graphql.emit(mockGraphQL);
await flushPromises();

// Simulate a selection removal
const pillContainer = element.shadowRoot.querySelector(
'lightning-pill-container'
);
pillContainer.dispatchEvent(
new CustomEvent('itemremove', {
detail: {
item: {
name: '005xx000016QpSqAAK',
label: 'Amy Taylor',
iconName: 'standard:contact'
}
}
})
);
await flushPromises();

// no more filter with the selected record id
expect(recordPickerElement.filter.criteria).toEqual(
expect.arrayContaining([
{ fieldPath: 'Id', operator: 'nin', value: [] }
])
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<lightning-card
title="RecordPickerMultiValue"
class="slds-card__body_inner"
icon-name="standard:search"
>
<div>
<lightning-record-picker
lwc:ref="recordPicker"
label="Contacts"
placeholder="Search Contacts..."
object-api-name="Contact"
filter={recordPickerFilter}
onchange={handleRecordPickerChange}
></lightning-record-picker>
</div>

<lightning-pill-container
items={pillItems}
onitemremove={handlePillRemove}
></lightning-pill-container>
<template lwc:if={wireError}>
<c-error-panel errors={wireError}></c-error-panel>
</template>

<c-view-source source="lwc/recordPickerMultiValue" slot="footer">
As of Winter '24, `lightning-record-picker` only supports a single
selection. This sample component shows how you can turn
`lightning-record-picker` into a multi-selection record picker.
</c-view-source>
</lightning-card>
</template>
Loading

0 comments on commit 4f57051

Please sign in to comment.