Skip to content

Commit 33fdd43

Browse files
authored
Merge pull request #1052 from Whiteshark57/useSessionStorage#998
Add useSessionStorage, onPanelSelectionChange and selectChildrenIfParentSelected to Taxonomy Picker Control
2 parents df6445b + 00fc824 commit 33fdd43

File tree

6 files changed

+162
-33
lines changed

6 files changed

+162
-33
lines changed

src/common/utilities/LocalesHelper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { findIndex } from '@microsoft/sp-lodash-subset';
2+
13
export class LocalesHelper {
24

35
private static locales = {
@@ -53,7 +55,7 @@ export class LocalesHelper {
5355
10266: 'sr-Cyrl-RS',
5456
};
5557
public static getLocaleId(localeName: string): number {
56-
const pos: number = Object.keys(this.locales).findIndex(locKey => this.locales[locKey] === localeName);
58+
const pos: number = findIndex(Object.keys(this.locales), locKey => this.locales[locKey] === localeName);
5759
if (pos > -1) {
5860
return parseInt(Object.keys(this.locales)[pos]);
5961
}

src/controls/listView/ListView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class ListView extends React.Component<IListViewProps, IListViewState> {
2525
private dropArea = null;
2626
private dropRef = element => {
2727
this.dropArea = element;
28-
};
28+
}
2929

3030
constructor(props: IListViewProps) {
3131
super(props);

src/controls/taxonomyPicker/ITaxonomyPicker.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IPickerTerms } from './ITermPicker';
1+
import { IPickerTerm, IPickerTerms } from './ITermPicker';
22
import { ITermSet, ITerm } from '../../services/ISPTermStorePickerService';
33
import { IWebPartContext } from '@microsoft/sp-webpart-base';
44
import { ITermActions } from './termActions/ITermsActions';
@@ -120,6 +120,23 @@ export interface ITaxonomyPickerProps {
120120
* Note that error message should be specified in onGetErrorMessage
121121
*/
122122
required?: boolean;
123+
124+
/**
125+
* Specifies if you want to use session storage
126+
* Default will be true
127+
*/
128+
useSessionStorage?: boolean;
129+
130+
/**
131+
* Panel selection change handler. Can be used to interact with the control while selecting items in the panel, before Click or Cancel is clicked.
132+
*/
133+
onPanelSelectionChange?: (prevValue: IPickerTerms, newValue: IPickerTerms) => void;
134+
135+
/**
136+
* Specifies if the childrens should be selected when parent is selected.
137+
* By default this is set to false.
138+
*/
139+
selectChildrenIfParentSelected?: boolean;
123140
}
124141

125142
/**

src/controls/taxonomyPicker/TaxonomyPicker.tsx

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import { sortBy, cloneDeep, isEqual } from '@microsoft/sp-lodash-subset';
1515
import uniqBy = require('lodash/uniqBy');
1616
import TermParent from './TermParent';
1717
import FieldErrorMessage from './ErrorMessage';
18-
18+
import { initializeIcons } from '@uifabric/icons';
1919
import * as telemetry from '../../common/telemetry';
20+
import { EmptyGuid } from '../../common/Constants';
2021

2122
/**
2223
* Image URLs / Base64
@@ -27,6 +28,8 @@ export const GROUP_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQ
2728
export const TERMSET_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACaSURBVDhPrZLRCcAgDERdpZMIjuQA7uWH4CqdxMY0EQtNjKWB0A/77sxF55SKMTalk8a61lqCFqsLiwKac84ZRUUBi7MoYHVmAfjfjzE6vJqZQfie0AcwBQVW8ATi7AR7zGGGNSE6Q2cyLSPIjRswjO7qKhcPDN2hK46w05wZMcEUIG+HrzzcrRsQBIJ5hS8C9fGAPmRwu/9RFxW6L8CM4Ry8AAAAAElFTkSuQmCC'; // /_layouts/15/Images/EMMTermSet.png
2829
export const TERM_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACzSURBVDhPY2AYNKCoqIgTiOcD8X8S8F6wB4Aa1IH4akNDw+mPHz++/E8EuHTp0jmQRSDNCcXFxa/XrVt3gAh9KEpgBvx/9OjRLVI1g9TDDYBp3rlz5//Kysr/IJoYgGEASPPatWsbQDQxAMOAbdu2gZ0FookBcAOePHlyhxgN6GqQY+Hdhg0bDpJqCNgAaDrQAnJuNDY2nvr06dMbYgw6e/bsabgBUEN4yEiJ2wdNViLfIQC3sTh2vtJcswAAAABJRU5ErkJggg==';
2930

31+
initializeIcons();
32+
3033
/**
3134
* Renders the controls for PropertyFieldTermPicker component
3235
*/
@@ -89,7 +92,7 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
8992
};
9093
}
9194

92-
if (nextProps.errorMessage) {
95+
if (nextProps.errorMessage !== this.props.errorMessage) {
9396
if (!newState) {
9497
newState = {};
9598
}
@@ -111,7 +114,8 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
111114
hideTagsNotAvailableForTagging,
112115
initialValues,
113116
validateOnLoad,
114-
termsetNameOrID
117+
termsetNameOrID,
118+
useSessionStorage
115119
} = this.props;
116120

117121
let isValidateOnLoad = validateOnLoad && initialValues && initialValues.length >= 1;
@@ -120,7 +124,7 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
120124
const notFoundTerms: string[] = [];
121125
const notFoundTermIds: string[] = [];
122126

123-
const termSet = await this.termsService.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging);
127+
const termSet = await this.termsService.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging, useSessionStorage);
124128
const allTerms = termSet.Terms;
125129

126130
for (let i = 0, len = initialValues.length; i < len; i++) {
@@ -152,7 +156,7 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
152156
// });
153157
}
154158

155-
this.termsService.getAllTerms(this.props.termsetNameOrID, this.props.hideDeprecatedTags, this.props.hideTagsNotAvailableForTagging).then((response: ITermSet) => {
159+
this.termsService.getAllTerms(this.props.termsetNameOrID, this.props.hideDeprecatedTags, this.props.hideTagsNotAvailableForTagging, this.props.useSessionStorage).then((response: ITermSet) => {
156160
// Check if a response was retrieved
157161
let termSetAndTerms = response ? response : null;
158162
this.setState({
@@ -166,7 +170,7 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
166170
* Force update of the taxonomy tree - required by term action in case the term has been added, deleted or moved.
167171
*/
168172
private async updateTaxonomyTree(): Promise<void> {
169-
const termSetAndTerms = await this.termsService.getAllTerms(this.props.termsetNameOrID, this.props.hideDeprecatedTags, this.props.hideTagsNotAvailableForTagging);
173+
const termSetAndTerms = await this.termsService.getAllTerms(this.props.termsetNameOrID, this.props.hideDeprecatedTags, this.props.hideTagsNotAvailableForTagging, this.props.useSessionStorage);
170174

171175
this.setState({
172176
termSetAndTerms
@@ -229,11 +233,20 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
229233
*/
230234
private termsChanged(term: ITerm, checked: boolean): void {
231235

232-
let activeNodes = this.state.activeNodes;
236+
let activeNodes = this.state.activeNodes.slice();
233237
if (typeof term === 'undefined' || term === null) {
234238
return;
235239
}
236240

241+
const {
242+
allowMultipleSelections,
243+
selectChildrenIfParentSelected
244+
} = this.props;
245+
246+
const {
247+
termSetAndTerms
248+
} = this.state;
249+
237250
// Term item to add to the active nodes array
238251
const termItem = {
239252
name: term.Name,
@@ -242,24 +255,58 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
242255
termSet: term.TermSet.Id
243256
};
244257

258+
// Check if we need to process child terms
259+
let children: ITerm[] = [];
260+
if (allowMultipleSelections && selectChildrenIfParentSelected) {
261+
if (term.Id === term.TermSet.Id) {
262+
children = termSetAndTerms.Terms || [];
263+
} else {
264+
children = termSetAndTerms.Terms ? termSetAndTerms.Terms.filter(t => {
265+
return t.PathOfTerm.indexOf(`${term.PathOfTerm}`) !== -1;
266+
}) : [];
267+
}
268+
}
269+
245270
// Check if the term is checked or unchecked
246271
if (checked) {
247272
// Check if it is allowed to select multiple terms
248-
if (this.props.allowMultipleSelections) {
273+
if (allowMultipleSelections) {
249274
// Add the checked term
250275
activeNodes.push(termItem);
251-
// Filter out the duplicate terms
252-
activeNodes = uniqBy(activeNodes, 'key');
253276
} else {
254277
// Only store the current selected item
255278
activeNodes = [termItem];
256279
}
280+
281+
if (children.length) {
282+
activeNodes.push(...children.map(c => {
283+
return {
284+
name: c.Name,
285+
key: c.Id,
286+
path: c.PathOfTerm,
287+
termSet: c.TermSet.Id
288+
};
289+
}));
290+
}
291+
292+
// Filter out the duplicate terms
293+
activeNodes = uniqBy(activeNodes, 'key');
257294
} else {
258295
// Remove the term from the list of active nodes
259296
activeNodes = activeNodes.filter(item => item.key !== term.Id);
297+
298+
if (children.length) {
299+
const childIds = children.map(c => c.Id);
300+
activeNodes = activeNodes.filter(item => childIds.indexOf(item.key) === -1);
301+
}
260302
}
261303
// Sort all active nodes
262304
activeNodes = sortBy(activeNodes, 'path');
305+
306+
if (this.props.onPanelSelectionChange) {
307+
this.props.onPanelSelectionChange(this.state.activeNodes.slice(), activeNodes);
308+
}
309+
263310
// Update the current state
264311
this.setState({
265312
activeNodes: activeNodes
@@ -312,22 +359,51 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
312359
return input;
313360
}
314361

362+
private async validateOnGetErrorMessage(targetValue: string): Promise<boolean> {
363+
const errorMessage = await this.props.onGetErrorMessage(
364+
[
365+
{
366+
key: EmptyGuid,
367+
name: targetValue,
368+
path: targetValue,
369+
termSet: this.termsService.cleanGuid(this.props.termsetNameOrID)
370+
}
371+
]
372+
);
373+
374+
if (!!errorMessage) {
375+
this.setState({
376+
errorMessage: errorMessage
377+
});
378+
} else {
379+
this.setState({
380+
errorMessage: null
381+
});
382+
}
383+
384+
return !errorMessage;
385+
}
386+
315387
/**
316388
* Triggers when taxonomy picker control loses focus
317389
*/
318-
private onBlur(event: React.FocusEvent<HTMLElement | Autofill>): void {
390+
private async onBlur(event: React.FocusEvent<HTMLElement | Autofill>): Promise<void> {
319391
const { validateInput } = this.props;
320392
if (!!validateInput) {
321393
// Perform validation of input text, only if taxonomy picker is configured with validateInput={true} property.
322394
const target: HTMLInputElement = event.target as HTMLInputElement;
323395
const targetValue = !!target ? target.value : null;
324-
if (!!targetValue) {
325-
this.invalidTerm = targetValue;
326-
}
327-
else {
328-
this.invalidTerm = null;
396+
if (!!this.props.onGetErrorMessage && !!targetValue) {
397+
await this.validateOnGetErrorMessage(targetValue);
398+
} else {
399+
if (!!targetValue) {
400+
this.invalidTerm = targetValue;
401+
}
402+
else {
403+
this.invalidTerm = null;
404+
}
405+
this.validateInputText();
329406
}
330-
this.validateInputText();
331407
}
332408
}
333409

@@ -403,6 +479,9 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
403479
if (typeof result === 'string') {
404480
if (!result) {
405481
this.validated(value);
482+
this.setState({
483+
errorMessage: undefined
484+
});
406485
}
407486
else {
408487
this.setState({
@@ -416,6 +495,9 @@ export class TaxonomyPicker extends React.Component<ITaxonomyPickerProps, ITaxon
416495

417496
if (!resolvedResult) {
418497
this.validated(value);
498+
this.setState({
499+
errorMessage: undefined
500+
});
419501
}
420502
else {
421503
this.setState({

src/services/SPTermStorePickerService.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export default class SPTermStorePickerService {
149149
* Retrieve all terms for the given term set
150150
* @param termset
151151
*/
152-
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean): Promise<ITermSet> {
152+
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<ITermSet> {
153153
if (Environment.type === EnvironmentType.Local) {
154154
// If the running environment is local, load the data from the mock
155155
return this.getAllMockTerms();
@@ -168,7 +168,7 @@ export default class SPTermStorePickerService {
168168
}
169169
}
170170

171-
let childTerms = this.getTermsById(termsetId);
171+
let childTerms = this.getTermsById(termsetId, useSessionStorage);
172172

173173
if (childTerms) {
174174
return childTerms;
@@ -231,7 +231,13 @@ export default class SPTermStorePickerService {
231231
}
232232
}
233233

234-
sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet));
234+
try {
235+
if (useSessionStorage && window.sessionStorage) {
236+
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet));
237+
}
238+
} catch (error) {
239+
// Do nothing, sometimes storage quota exceed error if too many items
240+
}
235241
return termStoreResultTermSet;
236242
}
237243
return null;
@@ -282,12 +288,21 @@ export default class SPTermStorePickerService {
282288
}
283289
}
284290

285-
private getTermsById(termId) {
286-
var terms = sessionStorage.getItem(termId);
287-
if (terms)
288-
return JSON.parse(terms);
289-
else
291+
private getTermsById(termId, useSessionStorage: boolean = true) {
292+
try {
293+
if (useSessionStorage && window.sessionStorage) {
294+
let terms = window.sessionStorage.getItem(termId);
295+
if (terms)
296+
return JSON.parse(terms);
297+
else {
298+
return null;
299+
}
300+
} else {
301+
return null;
302+
}
303+
} catch (error) {
290304
return null;
305+
}
291306
}
292307

293308
private searchTermsBySearchText(terms, searchText) {
@@ -303,7 +318,8 @@ export default class SPTermStorePickerService {
303318
// If the running environment is local, load the data from the mock
304319
return SPTermStoreMockHttpClient.searchTermsByName(searchText);
305320
} else {
306-
var childTerms = this.getTermsById(termId);
321+
const { useSessionStorage } = this.props;
322+
let childTerms = this.getTermsById(termId, useSessionStorage);
307323
if (childTerms) {
308324
return this.searchTermsBySearchText(childTerms, searchText);
309325
}
@@ -318,7 +334,8 @@ export default class SPTermStorePickerService {
318334
termsetNameOrID,
319335
termId,
320336
hideDeprecatedTags,
321-
hideTagsNotAvailableForTagging);
337+
hideTagsNotAvailableForTagging,
338+
useSessionStorage);
322339

323340
if (terms) {
324341
return this.searchTermsBySearchText(terms, searchText);
@@ -332,7 +349,7 @@ export default class SPTermStorePickerService {
332349
/**
333350
* Retrieve all terms for the given term set and anchorId
334351
*/
335-
public async getAllTermsByAnchorId(termsetNameOrID: string, anchorId: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean): Promise<IPickerTerm[]> {
352+
public async getAllTermsByAnchorId(termsetNameOrID: string, anchorId: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<IPickerTerm[]> {
336353

337354
let returnTerms: IPickerTerm[] = [];
338355

@@ -343,7 +360,7 @@ export default class SPTermStorePickerService {
343360
returnTerms.push(this.convertTermToPickerTerm(term));
344361
});
345362
} else {
346-
const childTerms = this.getTermsById(anchorId);
363+
const childTerms = this.getTermsById(anchorId, useSessionStorage);
347364
if (childTerms) {
348365
return childTerms;
349366
}
@@ -361,7 +378,13 @@ export default class SPTermStorePickerService {
361378
returnTerms.push(this.convertTermToPickerTerm(term));
362379
});
363380

364-
sessionStorage.setItem(anchorId, JSON.stringify(returnTerms));
381+
try {
382+
if (useSessionStorage && window.sessionStorage) {
383+
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms));
384+
}
385+
} catch (error) {
386+
// Do nothing
387+
}
365388
}
366389
} else {
367390
terms.forEach(term => {

0 commit comments

Comments
 (0)