Skip to content

Commit e6ebb17

Browse files
committed
Merge branch 'dev' of https://github.com/mgwojciech/sp-dev-fx-controls-react into mgwojciech-dev
2 parents b815cd4 + 0fadd9b commit e6ebb17

16 files changed

+556
-15
lines changed

config/karma.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ module.exports = function (config) {
2020
};
2121
config.plugins.push(htmlReporter);
2222

23+
config.set({
24+
framework: ['jasmine']
25+
})
2326
// Add the remap-coverage - code coverage for the original files
2427
config.reporters.push('remap-coverage');
2528
config.coverageReporter = {
991 Bytes
Loading
1.71 KB
Loading
12.3 KB
Loading
10 KB
Loading
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# ComboBoxListItemPicker control
2+
3+
This control allows you to select one or more items from a list. The List can be filtered to allow select items from a subset of items The item selection is based from a column value. The control will suggest items based on the inserted value.
4+
5+
Here is an example of the control:
6+
7+
![ComboBoxComboBoxListItemPicker](../assets/ComboBoxListItemPicker_1.png)
8+
9+
![ComboBoxListItemPicker multiple selection](../assets/ComboBoxListItemPicker_Multi.png)
10+
11+
![ComboBoxListItemPicker selected Items](../assets/ComboBoxListItemPicker_Options_Single.png)
12+
13+
![ComboBoxListItemPicker selected Items (Multiple Options)](../assets/ComboBoxListItemPicker_Options_Multi.png)
14+
15+
## How to use this control in your solutions
16+
17+
- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency.
18+
- Import the control into your component:
19+
20+
```TypeScript
21+
import { ComboBoxListItemPicker } from '@pnp/spfx-controls-react/lib/ComboBoxListItemPicker';
22+
```
23+
- Use the `ComboBoxListItemPicker` control in your code as follows:
24+
25+
```TypeScript
26+
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
27+
columnInternalName='Title'
28+
keyColumnInternalName='Id'
29+
filter="Title eq 'SPFx'"
30+
itemLimit={10}
31+
onSelectedItem={this.onSelectedItem}
32+
webUrl: this.context.pageContext.web.absoluteUrl,
33+
spHttpClient: new SPHttpClient() />
34+
```
35+
36+
- Use the `ComboBoxListItemPicker` with objects passed in defaultSelectedItems
37+
```TypeScript
38+
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
39+
columnInternalName='Title'
40+
keyColumnInternalName='Id'
41+
filter="Title eq 'SPFx'"
42+
itemLimit={10}
43+
defaultSelectedItems: [{Id: 2, Title:"Test"}]
44+
onSelectedItem={this.onSelectedItem}
45+
webUrl: this.context.pageContext.web.absoluteUrl,
46+
spHttpClient: new SPHttpClient() />
47+
```
48+
- Or only ids
49+
```TypeScript
50+
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
51+
columnInternalName='Title'
52+
keyColumnInternalName='Id'
53+
filter="Title eq 'SPFx'"
54+
itemLimit={10}
55+
defaultSelectedItems: [2]
56+
onSelectedItem={this.onSelectedItem}
57+
webUrl: this.context.pageContext.web.absoluteUrl,
58+
spHttpClient: new SPHttpClient() />
59+
```
60+
61+
- The `onSelectedItem` change event returns the list items selected and can be implemented as follows:
62+
63+
```TypeScript
64+
private onSelectedItem(data: { key: string; name: string }[]) {
65+
for (const item of data) {
66+
console.log(`Item value: ${item.key}`);
67+
console.log(`Item text: ${item.name}`);
68+
}
69+
}
70+
```
71+
## Implementation
72+
73+
The `ComboBoxListItemPicker` control can be configured with the following properties:
74+
75+
76+
| Property | Type | Required | Description |
77+
| ---- | ---- | ---- | ---- |
78+
| columnInternalName | string | yes | InternalName of column to search and get values. |
79+
| keyColumnInternalName | string | no | InternalName of column to use as the key for the selection. Must be a column with unique values. Default: Id |
80+
| webUrl | string | yes | Url to web hosting list |
81+
| spHttpClient | RequestClient | yes | Any implementation of PnPJS RequestClient |
82+
| listId | string | yes | Guid of the list. |
83+
| itemLimit | number | yes | Number of items which can be selected |
84+
| onSelectItem | (items: any[]) => void | yes | Callback function which returns the selected items. |
85+
| className | string | no | ClassName for the picker. |
86+
| webUrl | string | no | URL of the site. By default it uses the current site URL. |
87+
| defaultSelectedItems | any[] | no | Initial items that have already been selected and should appear in the people picker. Support objects and Ids only |
88+
| suggestionsHeaderText | string | no | The text that should appear at the top of the suggestion box. |
89+
| noResultsFoundText | string | no | The text that should appear when no results are returned. |
90+
| disabled | boolean | no | Specifies if the control is disabled or not. |
91+
| filter | string | no | Condition to filter list Item, same as $filter ODATA parameter|
92+
| multiSelect | boolean | no | Allows multiple selection|
93+
| onInitialized | () => void | no | Calls when component is ready|
94+
95+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/ComboBoxListItemPicker)

src/common/dal/ListItemRepository.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { RequestClient } from "@pnp/common/src/netutil";
2+
3+
export class ListItemRepository {
4+
constructor(protected SiteUrl: string, protected SPClient: RequestClient) {
5+
6+
}
7+
/**
8+
*
9+
* @param filterText text value of the filter part of oData query 'Id eq 1'
10+
* @param listId
11+
* @param internalColumnName
12+
* @param keyInternalColumnName
13+
* @param webUrl
14+
* @param top
15+
*/
16+
public async getListItemsByFilterClause(filterText: string, listId: string, internalColumnName: string, keyInternalColumnName?: string, webUrl?: string, top?: number): Promise<any[]> {
17+
let returnItems: any[];
18+
try {
19+
const webAbsoluteUrl = !webUrl ? this.SiteUrl : webUrl;
20+
const apiUrl = `${webAbsoluteUrl}/_api/web/lists('${listId}')/items?$select=${keyInternalColumnName || 'Id'},${internalColumnName}&$filter=${filterText}`;
21+
const data = await this.SPClient.get(apiUrl);
22+
if (data.ok) {
23+
const results = await data.json();
24+
if (results && results.value && results.value.length > 0) {
25+
return results.value;
26+
}
27+
}
28+
29+
return [];
30+
} catch (error) {
31+
return Promise.reject(error);
32+
}
33+
}
34+
}

src/common/mocks/RequestClientMock.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { RequestClient, FetchOptions } from "@pnp/common/src/netutil";
2+
3+
export class RequestClientMock implements RequestClient {
4+
public Requests: { url: string, method: string, options?: FetchOptions, resultString: string }[] = [];
5+
public OnRequest: (url: string, method: string, options?: FetchOptions) => void;
6+
public fetch(url: string, options?: FetchOptions): Promise<Response> {
7+
let mockedResponse = this.Requests.filter(req => req.method === options.method && req.url == url)[0];
8+
let response: Response;
9+
if (mockedResponse) {
10+
response = new Response(mockedResponse.resultString, {
11+
status: 200,
12+
statusText: "Ok"
13+
});
14+
}
15+
else {
16+
response = new Response(null, {
17+
status: 404,
18+
statusText: "Not fount",
19+
});
20+
}
21+
return Promise.resolve(response);
22+
}
23+
public fetchRaw(url: string, options?: FetchOptions): Promise<Response> {
24+
return this.fetch(url,options);
25+
}
26+
public get(url: string, options?: FetchOptions): Promise<Response> {
27+
options = options || {};
28+
options.method = "GET";
29+
return this.fetch(url,options);
30+
}
31+
public post(url: string, options?: FetchOptions): Promise<Response> {
32+
options = options || {};
33+
options.method = "POST";
34+
return this.fetch(url,options);
35+
}
36+
public patch(url: string, options?: FetchOptions): Promise<Response> {
37+
options = options || {};
38+
options.method = "PATCH";
39+
return this.fetch(url,options);
40+
}
41+
public delete(url: string, options?: FetchOptions): Promise<Response> {
42+
options = options || {};
43+
options.method = "DELETE";
44+
return this.fetch(url,options);
45+
}
46+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/// <reference types="sinon" />
2+
3+
import * as React from 'react';
4+
import { assert, expect } from 'chai';
5+
import { mount, ReactWrapper } from 'enzyme';
6+
import { ComboBoxListItemPicker } from './ComboBoxListItemPicker';
7+
import { RequestClientMock } from '../../common/mocks/RequestClientMock';
8+
9+
declare const sinon;
10+
11+
let mockHttpClient: RequestClientMock = new RequestClientMock();
12+
mockHttpClient.Requests.push({
13+
url: "/sites/test-site/_api/web/lists('TestId')/items?$select=Id,Title&$filter=Id gt 0",
14+
method: "GET",
15+
resultString: JSON.stringify({ "odata.metadata": "", "value": [{ "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "9624204a-c049-49a1-8a18-a417872f8883", "odata.etag": "\"2\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(1)", "Id": 1, "ID": 1, "Title": "Test 1" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "8b0e4ce8-f1e4-4b17-8f95-1ea7d63fefd6", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(2)", "Id": 2, "ID": 2, "Title": "Test 2" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "62aea125-e442-44cf-ab08-c555ffdd2798", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(3)", "Id": 3, "ID": 3, "Title": "Test 3" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "a25c55b2-9a4f-4976-8a12-f77393abe437", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(4)", "Id": 4, "ID": 4, "Title": "Test 4" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "8c57be67-a9a8-46c8-8954-6e51d3464be2", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(5)", "Id": 5, "ID": 5, "Title": "Test 5" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "d130664b-4e28-4ae5-b567-a3189f887922", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(6)", "Id": 6, "ID": 6, "Title": "Test 6" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "6e333b93-3f19-40ee-943c-19817193a3de", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(7)", "Id": 7, "ID": 7, "Title": "Test 7" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "a0063d1f-66ab-461b-8032-7dd38d7b8749", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(8)", "Id": 8, "ID": 8, "Title": "Test 8" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "8aca4c7c-4922-45e9-9f03-7d4a4bb6f926", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(9)", "Id": 9, "ID": 9, "Title": "Test 9" }, { "odata.type": "SP.Data.Test_x0020_listListItem", "odata.id": "f352a0c8-711b-47e4-9990-2a7121f961c3", "odata.etag": "\"1\"", "odata.editLink": "Web/Lists(guid'490fae3c-f8cb-4b50-9737-f2c94f6b6727')/Items(10)", "Id": 10, "ID": 10, "Title": "Test 10" }] })
16+
});
17+
describe('<ComboBoxListItemPicker />', () => {
18+
it("Should render initial data", () => {
19+
return new Promise((resolve, error) => {
20+
let comboBox = mount(<ComboBoxListItemPicker
21+
columnInternalName="Title"
22+
spHttpClient={mockHttpClient}
23+
webUrl="/sites/test-site"
24+
filter="Id gt 0"
25+
listId="TestId"
26+
itemLimit={20}
27+
onInitialized={() => {
28+
29+
expect(comboBox.state('availableOptions')).to.have.length(10);
30+
resolve();
31+
}}
32+
onSelectedItem={(item) => { }} />);
33+
});
34+
});
35+
it("Should call onSelectedItem", () => {
36+
return new Promise((resolve, error) => {
37+
let comboBox = mount(<ComboBoxListItemPicker
38+
columnInternalName="Title"
39+
spHttpClient={mockHttpClient}
40+
webUrl="/sites/test-site"
41+
filter="Id gt 0"
42+
listId="TestId"
43+
itemLimit={20}
44+
onInitialized={() => {
45+
46+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
47+
ddBtn.simulate('click');
48+
//actual list is not part of the component
49+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[3];
50+
(checkBoxBtn as HTMLButtonElement).click();
51+
52+
//ddBtn.simulate('click');
53+
}}
54+
onSelectedItem={(item) => {
55+
expect(item.Id).to.equal(4);
56+
57+
resolve();
58+
}} />);
59+
});
60+
});
61+
it("Should initialize with default selection (id)", () => {
62+
return new Promise((resolve, error) => {
63+
let comboBox = mount(<ComboBoxListItemPicker
64+
columnInternalName="Title"
65+
spHttpClient={mockHttpClient}
66+
webUrl="/sites/test-site"
67+
defaultSelectedItems={[1]}
68+
filter="Id gt 0"
69+
listId="TestId"
70+
itemLimit={20}
71+
onInitialized={() => {
72+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
73+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1");
74+
75+
resolve();
76+
}}
77+
onSelectedItem={(item) => {
78+
}} />);
79+
});
80+
});
81+
it("Should initialize with default selection (object)", () => {
82+
return new Promise((resolve, error) => {
83+
let comboBox = mount(<ComboBoxListItemPicker
84+
columnInternalName="Title"
85+
spHttpClient={mockHttpClient}
86+
webUrl="/sites/test-site"
87+
defaultSelectedItems={[{Id:1, Title: "Test 1"}]}
88+
filter="Id gt 0"
89+
listId="TestId"
90+
itemLimit={20}
91+
onInitialized={() => {
92+
93+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
94+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1");
95+
96+
resolve();
97+
}}
98+
onSelectedItem={(item) => {
99+
}} />);
100+
});
101+
});
102+
it("Should call onSelectedItem (multi)", () => {
103+
return new Promise((resolve, error) => {
104+
let comboBox = mount(<ComboBoxListItemPicker
105+
columnInternalName="Title"
106+
spHttpClient={mockHttpClient}
107+
webUrl="/sites/test-site"
108+
filter="Id gt 0"
109+
listId="TestId"
110+
itemLimit={20}
111+
multiSelect={true}
112+
onInitialized={() => {
113+
114+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
115+
ddBtn.simulate('click');
116+
//actual list is not part of the component
117+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[3];
118+
(checkBoxBtn as HTMLButtonElement).click();
119+
//ddBtn.simulate('click');
120+
}}
121+
onSelectedItem={(item) => {
122+
expect(item.Id).to.equal(4);
123+
expect(item.selected).to.be.equal(true);
124+
125+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
126+
ddBtn.simulate('click');
127+
resolve();
128+
}} />);
129+
});
130+
});
131+
it("Should initialize with default selection (multi) (object)", () => {
132+
return new Promise((resolve, error) => {
133+
let comboBox = mount(<ComboBoxListItemPicker
134+
columnInternalName="Title"
135+
spHttpClient={mockHttpClient}
136+
webUrl="/sites/test-site"
137+
defaultSelectedItems={[{Id:1, Title: "Test 1"},{ Id:2, Title: "Test 2"}]}
138+
filter="Id gt 0"
139+
listId="TestId"
140+
multiSelect={true}
141+
itemLimit={20}
142+
onInitialized={() => {
143+
144+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
145+
ddBtn.simulate('click');
146+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[0];
147+
expect(checkBoxBtn.classList.contains("is-checked")).to.be.equal(true);
148+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
149+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1, Test 2");
150+
ddBtn.simulate('click');
151+
resolve();
152+
}}
153+
onSelectedItem={(item) => {
154+
}} />);
155+
});
156+
});
157+
it("Should initialize with default selection (multi) (id)", () => {
158+
return new Promise((resolve, error) => {
159+
let comboBox = mount(<ComboBoxListItemPicker
160+
columnInternalName="Title"
161+
spHttpClient={mockHttpClient}
162+
webUrl="/sites/test-site"
163+
defaultSelectedItems={[1,2]}
164+
filter="Id gt 0"
165+
listId="TestId"
166+
itemLimit={20}
167+
multiSelect={true}
168+
onInitialized={() => {
169+
170+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
171+
ddBtn.simulate('click');
172+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[0];
173+
expect(checkBoxBtn.classList.contains("is-checked")).to.be.equal(true);
174+
175+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
176+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1, Test 2");
177+
ddBtn.simulate('click');
178+
resolve();
179+
}}
180+
onSelectedItem={(item) => {
181+
}} />);
182+
});
183+
});
184+
});

0 commit comments

Comments
 (0)