diff --git a/src/controls/taxonomyPicker/ITaxonomyPicker.ts b/src/controls/taxonomyPicker/ITaxonomyPicker.ts index 6afb7de73..9e4be97ca 100644 --- a/src/controls/taxonomyPicker/ITaxonomyPicker.ts +++ b/src/controls/taxonomyPicker/ITaxonomyPicker.ts @@ -35,6 +35,10 @@ export interface ITaxonomyPickerProps { * Id of a child term in the termset where to be able to selected and search the terms from */ ancoreId?: string; + /** + * Specify if the term set itself is selectable in the tree view + */ + isTermSetSelectable?: boolean; /** * Whether the property pane field is enabled or not. */ @@ -80,9 +84,12 @@ export interface ITermChanges { export interface ITermParentProps extends ITermChanges { termset: ITermSet; - autoExpand: () => void; multiSelection: boolean; anchorId? : string; + isTermSetSelectable?: boolean; + + autoExpand: () => void; + termSetSelectedChange?: (termSet: ITermSet, isChecked: boolean) => void; } export interface ITermParentState { diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.module.scss b/src/controls/taxonomyPicker/TaxonomyPicker.module.scss index c67d90b20..5956cc045 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.module.scss +++ b/src/controls/taxonomyPicker/TaxonomyPicker.module.scss @@ -34,6 +34,17 @@ margin-left: 15px; } +.termSetSelectable { + height: 50px; + line-height: 50px; +} + +.termSetSelector { + display: inline-block; + margin: 0 8px 0 4px; + vertical-align: middle; +} + .term { padding-left: 20px; @@ -72,8 +83,8 @@ width: 100%; text-align: left; cursor: pointer; - - + + .termSuggestionSubTitle { font-size: 12px; @@ -129,7 +140,7 @@ display: flex; align-items: center; } - + .errorIcon { font-size: 14px; margin-right: 5px; diff --git a/src/controls/taxonomyPicker/TaxonomyPicker.tsx b/src/controls/taxonomyPicker/TaxonomyPicker.tsx index 306b9beb0..dfa543f1b 100644 --- a/src/controls/taxonomyPicker/TaxonomyPicker.tsx +++ b/src/controls/taxonomyPicker/TaxonomyPicker.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { IWebPartContext } from '@microsoft/sp-webpart-base'; -import { Async } from 'office-ui-fabric-react/lib/Utilities'; import { PrimaryButton, DefaultButton, IconButton, IButtonProps } from 'office-ui-fabric-react/lib/Button'; import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel'; import { Spinner, SpinnerType } from 'office-ui-fabric-react/lib/Spinner'; @@ -32,7 +31,6 @@ export const TERM_IMG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQC * Renders the controls for PropertyFieldTermPicker component */ export class TaxonomyPicker extends React.Component { - private async: Async; private delayedValidate: (value: IPickerTerms) => void; private termsService: SPTermStorePickerService; private previousValues: IPickerTerms = []; @@ -58,7 +56,6 @@ export class TaxonomyPicker extends React.Component { + const ts: ITermSet = {...termSet}; + // Clean /Guid.../ from the ID + ts.Id = this.termsService.cleanGuid(ts.Id); + // Create a term for the termset + const term: ITerm = { + Name: ts.Name, + Id: ts.Id, + TermSet: ts, + PathOfTerm: "", + _ObjectType_: ts._ObjectType_, + _ObjectIdentity_: ts._ObjectIdentity_, + Description: ts.Description, + IsDeprecated: null, + IsRoot: null + }; + + // Trigger the normal change event + this.termsChanged(term, isChecked); } /** @@ -227,6 +241,7 @@ export class TaxonomyPicker extends React.Component @@ -268,6 +283,8 @@ export class TaxonomyPicker extends React.Component diff --git a/src/controls/taxonomyPicker/TermParent.tsx b/src/controls/taxonomyPicker/TermParent.tsx index 732c8841d..1cd044ae8 100644 --- a/src/controls/taxonomyPicker/TermParent.tsx +++ b/src/controls/taxonomyPicker/TermParent.tsx @@ -6,6 +6,8 @@ import { EXPANDED_IMG, COLLAPSED_IMG, TERMSET_IMG, TERM_IMG } from './TaxonomyPi import Term from './Term'; import styles from './TaxonomyPicker.module.scss'; +import { Checkbox } from 'office-ui-fabric-react'; +import * as strings from 'ControlStrings'; /** * Term Parent component, represents termset or term if anchorId @@ -17,6 +19,7 @@ export default class TermParent extends React.Component t.PathOfTerm.substring(0, anchorTerm.PathOfTerm.length) === anchorTerm.PathOfTerm && t.Id !== anchorTerm.Id); - + anchorTerms = anchorTerms.map(term => { term.PathDepth = term.PathDepth - anchorTerm.PathDepth; - + return term; }); @@ -64,6 +55,24 @@ export default class TermParent extends React.Component, isChecked: boolean): void => { + this.props.termSetSelectedChange(this.props.termset, isChecked); + } + + public render(): JSX.Element { // Specify the inline styling to show or hide the termsets const styleProps: React.CSSProperties = { @@ -72,7 +81,7 @@ export default class TermParent extends React.Component; // Check if the terms have been loaded - + if (this.state.loaded) { if (this._terms.length > 0) { termElm = ( @@ -85,18 +94,30 @@ export default class TermParent extends React.Component ); } else { - termElm =
Term set does not contain any terms
; + termElm =
{strings.TaxonomyPickerNoTerms}
; } } else { termElm = ; } - + return (
-
- Expand This Term Set - Menu for Term Set {this.props.anchorId ? this._anchorName : this.props.termset.Name} +
+ {strings.TaxonomyPickerExpandTitle} + { + // Show the termset selection box + (!this.props.anchorId && this.props.isTermSetSelectable) && + a.path === "" && a.key === a.termSet).length >= 1} + onChange={this.termSetSelectionChange} /> + } + {strings.TaxonomyPickerMenuTermSet} + { + this.props.anchorId ? + this._anchorName : + this.props.termset.Name + }
{termElm} diff --git a/src/controls/taxonomyPicker/TermPicker.tsx b/src/controls/taxonomyPicker/TermPicker.tsx index a02affd1f..06914f9af 100644 --- a/src/controls/taxonomyPicker/TermPicker.tsx +++ b/src/controls/taxonomyPicker/TermPicker.tsx @@ -5,6 +5,7 @@ import SPTermStorePickerService from './../../services/SPTermStorePickerService' import styles from './TaxonomyPicker.module.scss'; import { ITaxonomyPickerProps } from './ITaxonomyPicker'; import { IWebPartContext } from '@microsoft/sp-webpart-base'; +import * as strings from 'ControlStrings'; export class TermBasePicker extends BasePicker> { @@ -20,8 +21,9 @@ export interface ITermPickerProps { context: IWebPartContext; disabled: boolean; value: IPickerTerms; - onChanged: (items: IPickerTerm[]) => void; allowMultipleSelections : boolean; + isTermSetSelectable?: boolean; + onChanged: (items: IPickerTerm[]) => void; } export default class TermPicker extends React.Component { @@ -91,7 +93,7 @@ export default class TermPicker extends React.Component
{term.name}
-
in {termParent}
+
{strings.TaxonomyPickerInLabel} {termParent ? termParent : strings.TaxonomyPickerTermSetLabel}
); } @@ -102,7 +104,22 @@ export default class TermPicker extends React.Component { if (filterText !== "") { let termsService = new SPTermStorePickerService(this.props.termPickerHostProps, this.props.context); - let terms = await termsService.searchTermsByName(filterText); + let terms: IPickerTerm[] = await termsService.searchTermsByName(filterText); + // Check if the termset can be selected + if (this.props.isTermSetSelectable) { + // Retrieve the current termset + const termSet = await termsService.getTermSet(); + // Check if termset was retrieved and if it contains the filter value + if (termSet && termSet.Name.toLowerCase().indexOf(filterText.toLowerCase()) === 0) { + // Add the termset to the suggestion list + terms.push({ + key: termsService.cleanGuid(termSet.Id), + name: termSet.Name, + path: "", + termSet: termsService.cleanGuid(termSet.Id) + }); + } + } // Filter out the terms which are already set const filteredTerms = []; for (const term of terms) { diff --git a/src/loc/en-us.ts b/src/loc/en-us.ts index c4cdff11c..18ad597bd 100644 --- a/src/loc/en-us.ts +++ b/src/loc/en-us.ts @@ -38,6 +38,12 @@ define([], () => { "SendEmailTo": "Send an email to {0}", "StartChatWith": "Start a chat with {0}", "Contact": "Contact", - "UpdateProfile": "Update your profile" + "UpdateProfile": "Update your profile", + + "TaxonomyPickerNoTerms": "Term set does not contain any terms", + "TaxonomyPickerExpandTitle": "Expand This Term Set", + "TaxonomyPickerMenuTermSet": "Menu for Term Set", + "TaxonomyPickerInLabel": "in", + "TaxonomyPickerTermSetLabel": "Term Set" }; }); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index cec683743..74482a912 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -8,6 +8,13 @@ declare interface IControlStrings { StartChatWith: string; Contact: string; UpdateProfile: string; + + // Taxonomy picker + TaxonomyPickerNoTerms: string; + TaxonomyPickerExpandTitle: string; + TaxonomyPickerMenuTermSet: string; + TaxonomyPickerInLabel: string; + TaxonomyPickerTermSetLabel: string; } declare module 'ControlStrings' { diff --git a/src/services/SPTermStorePickerService.ts b/src/services/SPTermStorePickerService.ts index 6e2d10a09..b2592c25d 100644 --- a/src/services/SPTermStorePickerService.ts +++ b/src/services/SPTermStorePickerService.ts @@ -68,7 +68,7 @@ export default class SPTermStorePickerService { if (this.props.termsetNameOrID) { const termsetNameOrId = this.props.termsetNameOrID; termGroups = termGroups.map((group: IGroup) => { - group.TermSets._Child_Items_ = group.TermSets._Child_Items_.filter((termSet: ITermSet) => termSet.Name === termsetNameOrId || this._cleanGuid(termSet.Id).toLowerCase() === this._cleanGuid(termsetNameOrId).toLowerCase()); + group.TermSets._Child_Items_ = group.TermSets._Child_Items_.filter((termSet: ITermSet) => termSet.Name === termsetNameOrId || this.cleanGuid(termSet.Id).toLowerCase() === this.cleanGuid(termsetNameOrId).toLowerCase()); return group; }); } @@ -94,6 +94,19 @@ export default class SPTermStorePickerService { } } + /** + * Gets the current term set + */ + public async getTermSet(): Promise { + if (Environment.type === EnvironmentType.Local) { + const termSetInfo = await SPTermStoreMockHttpClient.getAllTerms(); + return termSetInfo; + } else { + const termStore = await this.getTermStores(); + return this.getTermSetId(termStore, this.props.termsetNameOrID); + } + } + /** * Retrieve all terms for the given term set * @param termset @@ -109,9 +122,9 @@ export default class SPTermStorePickerService { // Fetch the term store information const termStore = await this.getTermStores(); // Get the ID of the provided term set name - termsetId = this.getTermSetId(termStore, termset); - if (termsetId) { - termsetId = this._cleanGuid(termsetId); + const crntTermSet = this.getTermSetId(termStore, termset); + if (crntTermSet) { + termsetId = this.cleanGuid(crntTermSet.Id); } else { return null; } @@ -144,12 +157,12 @@ export default class SPTermStorePickerService { let terms = termStoreResultTerms[0]._Child_Items_; // Clean the term ID and specify the path depth terms = terms.map(term => { - term.Id = this._cleanGuid(term.Id); + term.Id = this.cleanGuid(term.Id); term['PathDepth'] = term.PathOfTerm.split(';').length; - term.TermSet = { Id : this._cleanGuid(termStoreResultTermSet.Id), Name : termStoreResultTermSet.Name}; + term.TermSet = { Id : this.cleanGuid(termStoreResultTermSet.Id), Name : termStoreResultTermSet.Name}; if (term["Parent"]) { - term.ParentId = this._cleanGuid(term["Parent"].Id); + term.ParentId = this.cleanGuid(term["Parent"].Id); } return term; }); @@ -174,7 +187,7 @@ export default class SPTermStorePickerService { * @param termstore * @param termset */ - private getTermSetId(termstore: ITermStore[], termsetName: string): string { + private getTermSetId(termstore: ITermStore[], termsetName: string): ITermSet { if (termstore && termstore.length > 0 && termsetName) { // Get the first term store const ts = termstore[0]; @@ -186,7 +199,7 @@ export default class SPTermStorePickerService { for (const termSet of group.TermSets._Child_Items_) { // Check if the term set is found if (termSet.Name === termsetName) { - return termSet.Id; + return termSet; } } } @@ -226,9 +239,9 @@ export default class SPTermStorePickerService { let TermSetId = termSet; if (!this.isGuid(termSet)) { // Get the ID of the provided term set name - TermSetId = this.getTermSetId(termStore, termSet); - if (TermSetId) { - TermSetId = this._cleanGuid(TermSetId); + const crntTermSet = this.getTermSetId(termStore, termSet); + if (crntTermSet) { + TermSetId = this.cleanGuid(crntTermSet.Id); } else { resolve(null); return; @@ -260,12 +273,11 @@ export default class SPTermStorePickerService { terms.forEach(term => { if (term.Name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1) { returnTerms.push({ - key:this._cleanGuid(term.Id), + key:this.cleanGuid(term.Id), name: term.Name, path: term.PathOfTerm, - termSet: this._cleanGuid(term.TermSet.Id), + termSet: this.cleanGuid(term.TermSet.Id), termSetName: term.TermSet.Name - }); } }); @@ -274,11 +286,7 @@ export default class SPTermStorePickerService { return null; }); }); - - }); - - }); } } @@ -306,7 +314,7 @@ export default class SPTermStorePickerService { * Clean the Guid from the Web Service response * @param guid */ - private _cleanGuid(guid: string): string { + public cleanGuid(guid: string): string { if (guid !== undefined) { return guid.replace('/Guid(', '').replace('/', '').replace(')', ''); } else { diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 59b2e4a60..702247f08 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -204,6 +204,7 @@ export default class ControlsTest extends React.Component
iframe dialog tester: