Skip to content

Commit 11e6807

Browse files
Mnickiigavinbarron
andauthored
feat: mgt-picker component for generic picking of entities from Microsoft Graph (#1937)
* update: add picker component * update: rename to mgt-picker * update: add picker story * update: get response from event * update: enable user provided placeholder * update: include placeholder in story * update: add max pages * update: add cache-enabled and key-name for title-property definition * update: add loading and error template, fix stories * update: fix cache-enabled * update: fire selectionChanged on click of option * update: add error template, remove unnecessary state updates * update: add event story * adds in rendered item template * include disambiguation * update: add complex rendered-item template * update: fix error template * Update stories/components/picker/picker.js * update: remove rendered-item template and story --------- Co-authored-by: Gavin Barron <[email protected]>
1 parent 8170289 commit 11e6807

File tree

9 files changed

+366
-2
lines changed

9 files changed

+366
-2
lines changed

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ <h2>mgt-todo</h2>
7373
<mgt-todo></mgt-todo>
7474
<h2>mgt-file-list</h2>
7575
<mgt-file-list></mgt-file-list>
76+
<h2>mgt-picker</h2>
77+
<mgt-picker resource="me/todo/lists" scopes="tasks.read, tasks.readwrite"></mgt-picker>
7678
</main>
7779
</body>
7880
</html>

packages/mgt-components/src/components/components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import './mgt-agenda/mgt-agenda';
99
import './mgt-file/mgt-file';
1010
import './mgt-file-list/mgt-file-list';
11+
import './mgt-picker/mgt-picker';
1112
import './mgt-get/mgt-get';
1213
import './mgt-login/mgt-login';
1314
import './mgt-people-picker/mgt-people-picker';
@@ -22,6 +23,7 @@ import './mgt-todo/mgt-todo';
2223
export * from './mgt-agenda/mgt-agenda';
2324
export * from './mgt-file/mgt-file';
2425
export * from './mgt-file-list/mgt-file-list';
26+
export * from './mgt-picker/mgt-picker';
2527
export * from './mgt-get/mgt-get';
2628
export * from './mgt-login/mgt-login';
2729
export * from './mgt-people-picker/mgt-people-picker';

packages/mgt-components/src/components/mgt-get/mgt-get.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ export class MgtGet extends MgtTemplatedComponent {
194194
*/
195195
@property({
196196
attribute: 'cache-invalidation-period',
197-
reflect: true,
198197
type: Number
199198
})
200199
public cacheInvalidationPeriod: number = 0;
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/**
2+
* -------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4+
* See License in the project root for license information.
5+
* -------------------------------------------------------------------------------------------
6+
*/
7+
8+
import { html, TemplateResult } from 'lit';
9+
import { property, state } from 'lit/decorators.js';
10+
import { MgtTemplatedComponent, mgtHtml, customElement } from '@microsoft/mgt-element';
11+
import { strings } from './strings';
12+
import { fluentCombobox, fluentOption } from '@fluentui/web-components';
13+
import { registerFluentComponents } from '../../utils/FluentComponents';
14+
import '../../styles/style-helper';
15+
16+
registerFluentComponents(fluentCombobox, fluentOption);
17+
18+
/**
19+
* Web component that allows a single entity pick from a generic endpoint from Graph. Uses mgt-get.
20+
*
21+
* @fires {CustomEvent<any>} selectionChanged - Fired when an option is clicked/selected
22+
* @export
23+
* @class MgtPicker
24+
* @extends {MgtTemplatedComponent}
25+
*/
26+
// @customElement('mgt-picker')
27+
@customElement('picker')
28+
export class MgtPicker extends MgtTemplatedComponent {
29+
protected get strings() {
30+
return strings;
31+
}
32+
33+
/**
34+
* The resource to get
35+
*
36+
* @type {string}
37+
* @memberof MgtPicker
38+
*/
39+
@property({
40+
attribute: 'resource',
41+
type: String
42+
})
43+
public resource: string;
44+
45+
/**
46+
* Api version to use for request
47+
*
48+
* @type {string}
49+
* @memberof MgtPicker
50+
*/
51+
@property({
52+
attribute: 'version',
53+
type: String
54+
})
55+
public version: string = 'v1.0';
56+
57+
/**
58+
* Maximum number of pages to get for the resource
59+
* default = 3
60+
* if <= 0, all pages will be fetched
61+
*
62+
* @type {number}
63+
* @memberof MgtPicker
64+
*/
65+
@property({
66+
attribute: 'max-pages',
67+
type: Number
68+
})
69+
public maxPages: number = 3;
70+
71+
/**
72+
* A placeholder for the picker
73+
*
74+
* @type {string}
75+
* @memberof MgtPicker
76+
*/
77+
@property({
78+
attribute: 'placeholder',
79+
type: String
80+
})
81+
public placeholder: string;
82+
83+
/**
84+
* Key to be rendered in the picker
85+
*
86+
* @type {string}
87+
* @memberof MgtPicker
88+
*/
89+
@property({
90+
attribute: 'key-name',
91+
type: String
92+
})
93+
public keyName: string;
94+
95+
/**
96+
* Entity to be rendered in the picker
97+
*
98+
* @type {string}
99+
* @memberof MgtPicker
100+
*/
101+
@property({
102+
attribute: 'entity-type',
103+
type: String
104+
})
105+
public entityType: string;
106+
107+
/**
108+
* The scopes to request
109+
*
110+
* @type {string[]}
111+
* @memberof MgtPicker
112+
*/
113+
@property({
114+
attribute: 'scopes',
115+
converter: value => {
116+
return value ? value.toLowerCase().split(',') : null;
117+
}
118+
})
119+
public scopes: string[] = [];
120+
121+
/**
122+
* Enables cache on the response from the specified resource
123+
* default = false
124+
*
125+
* @type {boolean}
126+
* @memberof MgtPicker
127+
*/
128+
@property({
129+
attribute: 'cache-enabled',
130+
type: Boolean
131+
})
132+
public cacheEnabled: boolean = false;
133+
134+
/**
135+
* Invalidation period of the cache for the responses in milliseconds
136+
*
137+
* @type {number}
138+
* @memberof MgtPicker
139+
*/
140+
@property({
141+
attribute: 'cache-invalidation-period',
142+
type: Number
143+
})
144+
public cacheInvalidationPeriod: number = 0;
145+
146+
private isRefreshing: boolean;
147+
148+
@state() private response: any[];
149+
@state() private error: any[];
150+
151+
constructor() {
152+
super();
153+
this.placeholder = this.strings.comboboxPlaceholder;
154+
this.entityType = null;
155+
this.keyName = null;
156+
this.isRefreshing = false;
157+
}
158+
159+
/**
160+
* Refresh the data
161+
*
162+
* @param {boolean} [hardRefresh=false]
163+
* if false (default), the component will only update if the data changed
164+
* if true, the data will be first cleared and reloaded completely
165+
* @memberof MgtPicker
166+
*/
167+
public refresh(hardRefresh = false) {
168+
this.isRefreshing = true;
169+
if (hardRefresh) {
170+
this.clearState();
171+
}
172+
this.requestStateUpdate(hardRefresh);
173+
}
174+
175+
/**
176+
* Clears the state of the component
177+
*
178+
* @protected
179+
* @memberof MgtPicker
180+
*/
181+
protected clearState(): void {
182+
this.response = null;
183+
this.error = null;
184+
}
185+
186+
/**
187+
* Invoked on each update to perform rendering the picker. This method must return
188+
* a lit-html TemplateResult. Setting properties inside this method will *not*
189+
* trigger the element to update.
190+
*/
191+
public render() {
192+
if (this.isLoadingState && !this.response) {
193+
return this.renderTemplate('loading', null);
194+
} else if (this.hasTemplate('error')) {
195+
return this.renderTemplate('error', this.error ? this.error : null, 'error');
196+
} else if (this.hasTemplate('no-data')) {
197+
return this.renderTemplate('no-data', null);
198+
}
199+
200+
return this.response?.length > 0 ? this.renderPicker() : this.renderGet();
201+
}
202+
203+
/**
204+
* Render picker.
205+
*
206+
* @protected
207+
* @returns {TemplateResult}
208+
* @memberof MgtPicker
209+
*/
210+
protected renderPicker(): TemplateResult {
211+
return mgtHtml`
212+
<fluent-combobox id="combobox" autocomplete="list" placeholder=${this.placeholder}>
213+
${this.response.map(
214+
item => html`
215+
<fluent-option value=${item.id} @click=${e => this.handleClick(e, item)}> ${
216+
item[this.keyName]
217+
} </fluent-option>`
218+
)}
219+
</fluent-combobox>
220+
`;
221+
}
222+
223+
/**
224+
* Render picker.
225+
*
226+
* @protected
227+
* @returns {TemplateResult}
228+
* @memberof MgtPicker
229+
*/
230+
protected renderGet(): TemplateResult {
231+
return mgtHtml`
232+
<mgt-get
233+
resource=${this.resource}
234+
version=${this.version}
235+
scopes=${this.scopes}
236+
max-pages=${this.maxPages}
237+
?cache-enabled=${this.cacheEnabled}
238+
?cache-invalidation-period=${this.cacheInvalidationPeriod}>
239+
</mgt-get>`;
240+
}
241+
242+
/**
243+
* load state into the component.
244+
*
245+
* @protected
246+
* @returns
247+
* @memberof MgtPicker
248+
*/
249+
protected async loadState() {
250+
if (!this.response) {
251+
let parent = this.renderRoot.querySelector('mgt-get');
252+
parent.addEventListener('dataChange', (e): void => this.handleDataChange(e));
253+
}
254+
this.isRefreshing = false;
255+
}
256+
257+
private handleDataChange(e: any): void {
258+
let response = e.detail.response.value;
259+
let error = e.detail.error ? e.detail.error : null;
260+
this.response = response;
261+
this.error = error;
262+
}
263+
264+
private handleClick(e: MouseEvent, item: any) {
265+
this.fireCustomEvent('selectionChanged', item);
266+
}
267+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* -------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4+
* See License in the project root for license information.
5+
* -------------------------------------------------------------------------------------------
6+
*/
7+
8+
export const strings = {
9+
comboboxPlaceholder: 'Select an item'
10+
};

stories/components/get.stories.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export const UsingImageType = () => html`
148148
`;
149149

150150
export const UsingCaching = () => html`
151-
<mgt-get resource="me" caching-enabled="true">
151+
<mgt-get resource="me" cache-enabled="true">
152152
<template>
153153
Hello {{ displayName }}
154154
</template>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* -------------------------------------------------------------------------------------------
3+
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4+
* See License in the project root for license information.
5+
* -------------------------------------------------------------------------------------------
6+
*/
7+
8+
import { html } from 'lit';
9+
import { withCodeEditor } from '../../../.storybook/addons/codeEditorAddon/codeAddon';
10+
11+
export default {
12+
title: 'Components / mgt-picker',
13+
component: 'mgt-picker',
14+
decorators: [withCodeEditor]
15+
};
16+
17+
export const picker = () => html`
18+
<mgt-picker resource="me/todo/lists" scopes="tasks.read, tasks.readwrite" placeholder="Select a task list" key-name="displayName"></mgt-picker>
19+
`;
20+
21+
export const MaxPages = () => html`
22+
<mgt-picker resource="me/messages" scopes="mail.read" placeholder="Select a message" key-name="subject" max-pages="2"></mgt-picker>
23+
`;
24+
25+
export const CacheEnabled = () => html`
26+
<mgt-picker resource="/groups" placeholder="Select a group" scopes="group.read.all" key-name="displayName" cache-enabled="true" cache-invalidation-period="50000"></mgt-picker>
27+
`;
28+
29+
export const events = () => html`
30+
<!-- Inspect to view log -->
31+
<mgt-picker resource="me/messages" scopes="mail.read" placeholder="Select a message" key-name="subject" max-pages="2"></mgt-picker>
32+
<script>
33+
document.querySelector('mgt-picker').addEventListener('selectionChanged', e => {
34+
console.log('selectedItem:', e.detail)
35+
});
36+
</script>
37+
`;

0 commit comments

Comments
 (0)