Skip to content

Commit c7d6e1e

Browse files
committed
Merge branch 'mgwojciech-dev' into dev
2 parents b815cd4 + edd094a commit c7d6e1e

18 files changed

+589
-17
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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
onSelectedItem={this.onSelectedItem}
31+
webUrl={this.context.pageContext.web.absoluteUrl}
32+
spHttpClient={this.context.spHttpClient} />
33+
```
34+
35+
- Use the `ComboBoxListItemPicker` with objects passed in defaultSelectedItems
36+
37+
```TypeScript
38+
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
39+
columnInternalName='Title'
40+
keyColumnInternalName='Id'
41+
filter="Title eq 'SPFx'"
42+
defaultSelectedItems=[{Id: 2, Title:"Test"}]
43+
onSelectedItem={this.onSelectedItem}
44+
webUrl={this.context.pageContext.web.absoluteUrl}
45+
spHttpClient={this.context.spHttpClient} />
46+
```
47+
48+
- Or only ids
49+
50+
```TypeScript
51+
<ComboBoxListItemPicker listId='da8daf15-d84f-4ab1-9800-7568f82fed3f'
52+
columnInternalName='Title'
53+
keyColumnInternalName='Id'
54+
filter="Title eq 'SPFx'"
55+
defaultSelectedItems: [2]
56+
onSelectedItem={this.onSelectedItem}
57+
webUrl={this.context.pageContext.web.absoluteUrl}
58+
spHttpClient={this.context.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+
| onSelectItem | (items: any[]) => void | yes | Callback function which returns the selected items. |
84+
| className | string | no | ClassName for the picker. |
85+
| webUrl | string | no | URL of the site. By default it uses the current site URL. |
86+
| defaultSelectedItems | any[] | no | Initial items that have already been selected and should appear in the people picker. Support objects and Ids only |
87+
| suggestionsHeaderText | string | no | The text that should appear at the top of the suggestion box. |
88+
| noResultsFoundText | string | no | The text that should appear when no results are returned. |
89+
| disabled | boolean | no | Specifies if the control is disabled or not. |
90+
| filter | string | no | Condition to filter list Item, same as $filter ODATA parameter|
91+
| multiSelect | boolean | no | Allows multiple selection|
92+
| onInitialized | () => void | no | Calls when component is ready|
93+
94+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/ComboBoxListItemPicker)

docs/documentation/docs/controls/SecurityTrimmedControl.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ import { SecurityTrimmedControl } from "@pnp/spfx-controls-react/lib/SecurityTri
5656
</SecurityTrimmedControl>
5757
```
5858

59+
**Show a control when the user doesn't have permissions**
60+
61+
```jsx
62+
<SecurityTrimmedControl context={this.props.context}
63+
level={PermissionLevel.remoteListOrLib}
64+
remoteSiteUrl="https://<tenant>.sharepoint.com/sites/<siteName>"
65+
relativeLibOrListUrl="/sites/<siteName>/<list-or-library-URL>"
66+
permissions={[SPPermission.addListItems]}
67+
noPermissionsControl={<p>SOrry, you don't have permissions to this list.</p>}>
68+
{/* Specify the components to load when user has the required permissions */}
69+
</SecurityTrimmedControl>
70+
```
71+
5972
## Implementation
6073
6174
The `SecurityTrimmedControl` can be configured with the following properties:
@@ -70,6 +83,8 @@ The `SecurityTrimmedControl` can be configured with the following properties:
7083
| folderPath | string | no | Specify the name of a folder to check the user permissions against. Will be overridden if itemId is present. |
7184
| itemId | number | no | Specify the ID of the item to check the user permissions against. Takes precedence over folder. |
7285
| className | string | no | Specify the className to be used on the parent element. |
86+
| noPermissionsControl | JSX.Element | no | Optional. Specify the control you want to render if user doesn't have permissions. |
87+
| showLoadingAnimation | boolean | no | Optional. Specify should render loading animation. |
7388

7489
The `PermissionLevel` enum has the following values:
7590

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 { SPHttpClient } from '@microsoft/sp-http';
2+
3+
export class ListItemRepository {
4+
constructor(protected SiteUrl: string, protected SPClient: SPHttpClient) {
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, SPHttpClient.configurations.v1);
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 { SPHttpClient, SPHttpClientConfiguration, ISPHttpClientOptions, SPHttpClientResponse } from '@microsoft/sp-http';
2+
3+
export class RequestClientMock extends SPHttpClient {
4+
public Requests: { url: string, method: string, options?: ISPHttpClientOptions, resultString: string }[] = [];
5+
public OnRequest: (url: string, method: string, options?: ISPHttpClientOptions) => void;
6+
public fetch(url: string, configuration: SPHttpClientConfiguration, options: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
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(new SPHttpClientResponse(response));
22+
}
23+
public fetchRaw(url: string, configuration: SPHttpClientConfiguration, options?: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
24+
return this.fetch(url, configuration, options);
25+
}
26+
public get(url: string, configuration: SPHttpClientConfiguration, options?: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
27+
options = options || {};
28+
options.method = "GET";
29+
return this.fetch(url, configuration, options);
30+
}
31+
public post(url: string, configuration: SPHttpClientConfiguration, options?: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
32+
options = options || {};
33+
options.method = "POST";
34+
return this.fetch(url, configuration, options);
35+
}
36+
public patch(url: string, configuration: SPHttpClientConfiguration, options?: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
37+
options = options || {};
38+
options.method = "PATCH";
39+
return this.fetch(url, configuration, options);
40+
}
41+
public delete(url: string, configuration: SPHttpClientConfiguration, options?: ISPHttpClientOptions): Promise<SPHttpClientResponse> {
42+
options = options || {};
43+
options.method = "DELETE";
44+
return this.fetch(url, configuration, options);
45+
}
46+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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(null);
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+
onInitialized={() => {
27+
28+
expect(comboBox.state('availableOptions')).to.have.length(10);
29+
resolve();
30+
}}
31+
onSelectedItem={(item) => { }} />);
32+
});
33+
});
34+
it("Should call onSelectedItem", () => {
35+
return new Promise((resolve, error) => {
36+
let comboBox = mount(<ComboBoxListItemPicker
37+
columnInternalName="Title"
38+
spHttpClient={mockHttpClient}
39+
webUrl="/sites/test-site"
40+
filter="Id gt 0"
41+
listId="TestId"
42+
onInitialized={() => {
43+
44+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
45+
ddBtn.simulate('click');
46+
//actual list is not part of the component
47+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[3];
48+
(checkBoxBtn as HTMLButtonElement).click();
49+
50+
//ddBtn.simulate('click');
51+
}}
52+
onSelectedItem={(item) => {
53+
expect(item.Id).to.equal(4);
54+
55+
resolve();
56+
}} />);
57+
});
58+
});
59+
it("Should initialize with default selection (id)", () => {
60+
return new Promise((resolve, error) => {
61+
let comboBox = mount(<ComboBoxListItemPicker
62+
columnInternalName="Title"
63+
spHttpClient={mockHttpClient}
64+
webUrl="/sites/test-site"
65+
defaultSelectedItems={[1]}
66+
filter="Id gt 0"
67+
listId="TestId"
68+
onInitialized={() => {
69+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
70+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1");
71+
72+
resolve();
73+
}}
74+
onSelectedItem={(item) => {
75+
}} />);
76+
});
77+
});
78+
it("Should initialize with default selection (object)", () => {
79+
return new Promise((resolve, error) => {
80+
let comboBox = mount(<ComboBoxListItemPicker
81+
columnInternalName="Title"
82+
spHttpClient={mockHttpClient}
83+
webUrl="/sites/test-site"
84+
defaultSelectedItems={[{Id:1, Title: "Test 1"}]}
85+
filter="Id gt 0"
86+
listId="TestId"
87+
onInitialized={() => {
88+
89+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
90+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1");
91+
92+
resolve();
93+
}}
94+
onSelectedItem={(item) => {
95+
}} />);
96+
});
97+
});
98+
it("Should call onSelectedItem (multi)", () => {
99+
return new Promise((resolve, error) => {
100+
let comboBox = mount(<ComboBoxListItemPicker
101+
columnInternalName="Title"
102+
spHttpClient={mockHttpClient}
103+
webUrl="/sites/test-site"
104+
filter="Id gt 0"
105+
listId="TestId"
106+
multiSelect={true}
107+
onInitialized={() => {
108+
109+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
110+
ddBtn.simulate('click');
111+
//actual list is not part of the component
112+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[3];
113+
(checkBoxBtn as HTMLButtonElement).click();
114+
//ddBtn.simulate('click');
115+
}}
116+
onSelectedItem={(item) => {
117+
expect(item.Id).to.equal(4);
118+
expect(item.selected).to.be.equal(true);
119+
120+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
121+
ddBtn.simulate('click');
122+
resolve();
123+
}} />);
124+
});
125+
});
126+
it("Should initialize with default selection (multi) (object)", () => {
127+
return new Promise((resolve, error) => {
128+
let comboBox = mount(<ComboBoxListItemPicker
129+
columnInternalName="Title"
130+
spHttpClient={mockHttpClient}
131+
webUrl="/sites/test-site"
132+
defaultSelectedItems={[{Id:1, Title: "Test 1"},{ Id:2, Title: "Test 2"}]}
133+
filter="Id gt 0"
134+
listId="TestId"
135+
multiSelect={true}
136+
onInitialized={() => {
137+
138+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
139+
ddBtn.simulate('click');
140+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[0];
141+
expect(checkBoxBtn.classList.contains("is-checked")).to.be.equal(true);
142+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
143+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1, Test 2");
144+
ddBtn.simulate('click');
145+
resolve();
146+
}}
147+
onSelectedItem={(item) => {
148+
}} />);
149+
});
150+
});
151+
it("Should initialize with default selection (multi) (id)", () => {
152+
return new Promise((resolve, error) => {
153+
let comboBox = mount(<ComboBoxListItemPicker
154+
columnInternalName="Title"
155+
spHttpClient={mockHttpClient}
156+
webUrl="/sites/test-site"
157+
defaultSelectedItems={[1,2]}
158+
filter="Id gt 0"
159+
listId="TestId"
160+
multiSelect={true}
161+
onInitialized={() => {
162+
163+
let ddBtn = comboBox.find('.ms-Button-flexContainer').first();
164+
ddBtn.simulate('click');
165+
let checkBoxBtn = document.querySelectorAll('.ms-ComboBox-option')[0];
166+
expect(checkBoxBtn.classList.contains("is-checked")).to.be.equal(true);
167+
168+
let ddInput = comboBox.find('.ms-ComboBox-Input').first();
169+
expect((ddInput.getNode() as any).value).to.be.equal("Test 1, Test 2");
170+
ddBtn.simulate('click');
171+
resolve();
172+
}}
173+
onSelectedItem={(item) => {
174+
}} />);
175+
});
176+
});
177+
});

0 commit comments

Comments
 (0)