Skip to content

Commit d7d86a2

Browse files
Changed from tag picker to term picker
1 parent e092701 commit d7d86a2

File tree

12 files changed

+626
-90
lines changed

12 files changed

+626
-90
lines changed

src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx

Lines changed: 120 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@ import * as React from 'react';
22
import { BaseComponentContext } from '@microsoft/sp-component-base';
33
import { Guid } from '@microsoft/sp-core-library';
44
import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon';
5-
import { PrimaryButton, DefaultButton, IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
5+
import { PrimaryButton,
6+
DefaultButton,
7+
IconButton,
8+
IButtonStyles
9+
} from 'office-ui-fabric-react/lib/Button';
610
import { Label } from 'office-ui-fabric-react/lib/Label';
7-
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
8-
import { IBasePickerStyleProps, IBasePickerStyles, ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers';
9-
import { IStackTokens, Stack } from 'office-ui-fabric-react/lib/Stack';
11+
import { Panel,
12+
PanelType
13+
} from 'office-ui-fabric-react/lib/Panel';
14+
import { IBasePickerStyleProps,
15+
IBasePickerStyles,
16+
ISuggestionItemProps
17+
} from 'office-ui-fabric-react/lib/Pickers';
18+
import { IStackTokens,
19+
Stack
20+
} from 'office-ui-fabric-react/lib/Stack';
1021
import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities';
1122
import { sp } from '@pnp/sp';
1223
import { SPTaxonomyService } from '../../services/SPTaxonomyService';
@@ -16,52 +27,74 @@ import * as strings from 'ControlStrings';
1627
import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle';
1728
import { useId } from '@uifabric/react-hooks';
1829
import { ITooltipHostStyles } from 'office-ui-fabric-react';
19-
import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy';
30+
import { ITermInfo,
31+
ITermSetInfo,
32+
ITermStoreInfo
33+
} from '@pnp/sp/taxonomy';
34+
import { TermItemSuggestion } from './termItem/TermItemSuggestion';
35+
import { ModernTermPicker } from './modernTermPicker/ModernTermPicker';
36+
import { ITermInfoExt,
37+
ITermItemProps
38+
} from './modernTermPicker/ModernTermPicker.types';
39+
import { TermItem } from './termItem/TermItem';
2040

2141
export interface IModernTaxonomyPickerProps {
22-
allowMultipleSelections: boolean;
42+
allowMultipleSelections?: boolean;
2343
termSetId: string;
2444
anchorTermId?: string;
2545
panelTitle: string;
2646
label: string;
2747
context: BaseComponentContext;
28-
initialValues?: ITag[];
48+
initialValues?: ITermInfo[];
2949
disabled?: boolean;
3050
required?: boolean;
31-
onChange?: (newValue?: ITag[]) => void;
51+
onChange?: (newValue?: ITermInfo[]) => void;
52+
onRenderItem?: (itemProps: ITermItemProps) => JSX.Element;
53+
onRenderSuggestionsItem?: (term: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>) => JSX.Element;
3254
placeHolder?: string;
55+
customPanelWidth?: number;
3356
}
3457

3558
export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
3659
const [taxonomyService] = React.useState(() => new SPTaxonomyService(props.context));
3760
const [panelIsOpen, setPanelIsOpen] = React.useState(false);
38-
const [selectedOptions, setSelectedOptions] = React.useState<ITag[]>(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []);
39-
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITag[]>([]);
40-
const [termStoreInfo, setTermStoreInfo] = React.useState<ITermStoreInfo>();
41-
const [termSetInfo, setTermSetInfo] = React.useState<ITermSetInfo>();
42-
const [anchorTermInfo, setAnchorTermInfo] = React.useState<ITermInfo>();
61+
const [selectedOptions, setSelectedOptions] = React.useState<ITermInfoExt[]>([]);
62+
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITermInfoExt[]>([]);
63+
const [currentTermStoreInfo, setCurrentTermStoreInfo] = React.useState<ITermStoreInfo>();
64+
const [currentTermSetInfo, setCurrentTermSetInfo] = React.useState<ITermSetInfo>();
65+
const [currentAnchorTermInfo, setCurrentAnchorTermInfo] = React.useState<ITermInfo>();
66+
const [currentLanguageTag, setCurrentLanguageTag] = React.useState<string>("");
4367

4468
React.useEffect(() => {
4569
sp.setup(props.context);
4670
taxonomyService.getTermStoreInfo()
47-
.then((localTermStoreInfo) => {
48-
setTermStoreInfo(localTermStoreInfo);
71+
.then((termStoreInfo) => {
72+
setCurrentTermStoreInfo(termStoreInfo);
73+
setCurrentLanguageTag(props.context.pageContext.cultureInfo.currentUICultureName !== '' ?
74+
props.context.pageContext.cultureInfo.currentUICultureName :
75+
currentTermStoreInfo.defaultLanguageTag);
76+
setSelectedOptions(Object.prototype.toString.call(props.initialValues) === '[object Array]' ?
77+
props.initialValues.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfoExt;}) :
78+
[]);
4979
});
5080
taxonomyService.getTermSetInfo(Guid.parse(props.termSetId))
51-
.then((localTermSetInfo) => {
52-
setTermSetInfo(localTermSetInfo);
81+
.then((termSetInfo) => {
82+
setCurrentTermSetInfo(termSetInfo);
5383
});
5484
if (props.anchorTermId && props.anchorTermId !== Guid.empty.toString()) {
5585
taxonomyService.getTermById(Guid.parse(props.termSetId), props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty)
56-
.then((localAnchorTermInfo) => {
57-
setAnchorTermInfo(localAnchorTermInfo);
86+
.then((anchorTermInfo) => {
87+
setCurrentAnchorTermInfo(anchorTermInfo);
5888
});
5989
}
6090
}, []);
6191

6292
React.useEffect(() => {
6393
if (props.onChange) {
64-
props.onChange(selectedOptions);
94+
props.onChange(selectedOptions.map(termInfoExt => {
95+
const {languageTag, termStoreInfo, ...termInfo} = termInfoExt;
96+
return termInfo;
97+
}));
6598
}
6699
}, [selectedOptions]);
67100

@@ -83,29 +116,69 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
83116
onClosePanel();
84117
}
85118

86-
async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise<ITag[]> {
87-
const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : termStoreInfo.defaultLanguageTag;
119+
async function onResolveSuggestions(filter: string, selectedItems?: ITermInfoExt[]): Promise<ITermInfoExt[]> {
88120
if (filter === '') {
89121
return [];
90122
}
91-
const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty);
123+
const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, currentLanguageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty);
92124
const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => {
93125
if (!selectedItems || selectedItems.length === 0) {
94126
return true;
95127
}
96-
return selectedItems.every((item) => item.key !== term.id);
128+
return selectedItems.every((item) => item.id !== term.id);
97129
});
98130
const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable);
99-
const filteredTags = filteredTermsAndAvailable.map((term) => {
100-
const key = term.id;
101-
let labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag));
131+
const filteredTermsAndAvailableAsExt = filteredTermsAndAvailable.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfoExt;});
132+
return filteredTermsAndAvailableAsExt;
133+
}
134+
135+
async function onLoadParentLabel(termId: Guid): Promise<string> {
136+
const termInfo = await taxonomyService.getTermById(Guid.parse(props.termSetId), termId);
137+
if (termInfo.parent) {
138+
let labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
102139
if (labelsWithMatchingLanguageTag.length === 0) {
103-
labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === termStoreInfo.defaultLanguageTag));
140+
labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
104141
}
105-
const name = labelsWithMatchingLanguageTag.filter((termLabel) => termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name;
106-
return { key: key, name: name };
107-
});
108-
return filteredTags;
142+
return labelsWithMatchingLanguageTag[0]?.name;
143+
}
144+
else {
145+
let termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentLanguageTag);
146+
if (termSetNames.length === 0) {
147+
termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag);
148+
}
149+
return termSetNames[0].name;
150+
}
151+
}
152+
153+
function onRenderSuggestionsItem(term: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>): JSX.Element {
154+
return (
155+
<TermItemSuggestion
156+
onLoadParentLabel={onLoadParentLabel}
157+
term={term}
158+
termStoreInfo={currentTermStoreInfo}
159+
languageTag={currentLanguageTag}
160+
{...itemProps}
161+
/>
162+
);
163+
}
164+
165+
function onRenderItem(itemProps: ITermItemProps): JSX.Element {
166+
let labels = itemProps.item.labels.filter((name) => name.languageTag === currentLanguageTag && name.isDefault);
167+
if (labels.length === 0) {
168+
labels = itemProps.item.labels.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag && name.isDefault);
169+
}
170+
171+
return labels.length > 0 ? (
172+
<TermItem languageTag={currentLanguageTag} termStoreInfo={currentTermStoreInfo} {...itemProps}>{labels[0].name}</TermItem>
173+
) : null;
174+
}
175+
176+
function getTextFromItem(termInfo: ITermInfoExt): string {
177+
let labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
178+
if (labelsWithMatchingLanguageTag.length === 0) {
179+
labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
180+
}
181+
return labelsWithMatchingLanguageTag[0]?.name;
109182
}
110183

111184
const calloutProps = { gapSpace: 0 };
@@ -119,22 +192,25 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
119192
{props.label && <Label required={props.required}>{props.label}</Label>}
120193
<div className={styles.termField}>
121194
<div className={styles.termFieldInput}>
122-
<TagPicker
195+
<ModernTermPicker
123196
removeButtonAriaLabel={strings.ModernTaxonomyPickerRemoveButtonText}
124197
onResolveSuggestions={onResolveSuggestions}
125198
itemLimit={props.allowMultipleSelections ? undefined : 1}
126199
selectedItems={selectedOptions}
127200
disabled={props.disabled}
128201
styles={tagPickerStyles}
129-
onChange={(itms?: ITag[]) => {
202+
onChange={(itms?: ITermInfoExt[]) => {
130203
setSelectedOptions(itms || []);
131204
setSelectedPanelOptions(itms || []);
132205
}}
133-
getTextFromItem={(tag: ITag) => tag.name}
206+
getTextFromItem={getTextFromItem}
207+
pickerSuggestionsProps={{noResultsFoundText: strings.ModernTaxonomyPickerNoResultsFound}}
134208
inputProps={{
135209
'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder,
136210
placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder
137211
}}
212+
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
213+
onRenderItem={props.onRenderItem ?? onRenderItem}
138214
/>
139215
</div>
140216
<div className={styles.termFieldButton}>
@@ -155,7 +231,8 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
155231
closeButtonAriaLabel={strings.ModernTaxonomyPickerPanelCloseButtonText}
156232
onDismiss={onClosePanel}
157233
isLightDismiss={true}
158-
type={PanelType.medium}
234+
type={props.customPanelWidth ? PanelType.custom : PanelType.medium}
235+
customWidth={props.customPanelWidth ? `${props.customPanelWidth}px` : undefined}
159236
headerText={props.panelTitle}
160237
onRenderFooterContent={() => {
161238
const horizontalGapStackTokens: IStackTokens = {
@@ -176,15 +253,19 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
176253
allowMultipleSelections={props.allowMultipleSelections}
177254
onResolveSuggestions={onResolveSuggestions}
178255
onLoadMoreData={taxonomyService.getTerms}
179-
anchorTermInfo={anchorTermInfo}
180-
termSetInfo={termSetInfo}
181-
termStoreInfo={termStoreInfo}
256+
anchorTermInfo={currentAnchorTermInfo}
257+
termSetInfo={currentTermSetInfo}
258+
termStoreInfo={currentTermStoreInfo}
182259
context={props.context}
183260
termSetId={Guid.parse(props.termSetId)}
184261
pageSize={50}
185262
selectedPanelOptions={selectedPanelOptions}
186263
setSelectedPanelOptions={setSelectedPanelOptions}
187264
placeHolder={props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder}
265+
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
266+
onRenderItem={props.onRenderItem ?? onRenderItem}
267+
getTextFromItem={getTextFromItem}
268+
languageTag={currentLanguageTag}
188269
/>
189270
</div>
190271
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './ModernTaxonomyPicker';
2+
export * from './termItem/TermItem';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from "react";
2+
import { BasePicker } from "office-ui-fabric-react/lib/components/pickers/BasePicker";
3+
import { IModernTermPickerProps,
4+
ITermInfoExt,
5+
ITermItemProps
6+
} from "./ModernTermPicker.types";
7+
import { TermItem } from "../termItem/TermItem";
8+
import { TermItemSuggestion } from "../termItem/TermItemSuggestion";
9+
import { IBasePickerStyleProps,
10+
IBasePickerStyles
11+
} from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
12+
import { getStyles } from "office-ui-fabric-react/lib/components/pickers/BasePicker.styles";
13+
import { initializeComponentRef,
14+
styled
15+
} from "office-ui-fabric-react/lib/Utilities";
16+
import { ISuggestionItemProps } from "office-ui-fabric-react/lib/components/pickers/Suggestions/SuggestionsItem.types";
17+
import { Guid } from "@microsoft/sp-core-library";
18+
19+
export class ModernTermPickerBase extends BasePicker<ITermInfoExt, IModernTermPickerProps> {
20+
public static defaultProps = {
21+
onRenderItem: (props: ITermItemProps) => {
22+
let labels = props.item.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault);
23+
if (labels.length === 0) {
24+
labels = props.item.labels.filter((name) => name.languageTag === props.termStoreInfo?.defaultLanguageTag && name.isDefault);
25+
}
26+
27+
return labels.length > 0 ? (
28+
<TermItem {...props}>{labels[0].name}</TermItem>
29+
) : null;
30+
},
31+
onRenderSuggestionsItem: (props: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>) => {
32+
const onLoadParentLabel = async (termId: Guid): Promise<string> => {
33+
return Promise.resolve("");
34+
};
35+
return <TermItemSuggestion term={props} languageTag={props.languageTag} onLoadParentLabel={onLoadParentLabel} termStoreInfo={props.termStoreInfo} {...itemProps} />;
36+
},
37+
};
38+
39+
constructor(props: IModernTermPickerProps) {
40+
super(props);
41+
initializeComponentRef(this);
42+
}
43+
}
44+
45+
export const ModernTermPicker = styled<IModernTermPickerProps, IBasePickerStyleProps, IBasePickerStyles>(
46+
ModernTermPickerBase,
47+
getStyles,
48+
undefined,
49+
{
50+
scope: 'ModernTermPicker',
51+
},
52+
);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { ITermInfo, ITermStoreInfo } from "@pnp/sp/taxonomy";
2+
import { IBasePickerProps } from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
3+
import { IPickerItemProps } from "office-ui-fabric-react/lib/components/pickers/PickerItem.types";
4+
import { IStyle, ITheme } from "office-ui-fabric-react/lib/Styling";
5+
import { IStyleFunctionOrObject } from "office-ui-fabric-react/lib/Utilities";
6+
7+
export interface ITermInfoExt extends ITermInfo {
8+
termStoreInfo: ITermStoreInfo;
9+
languageTag: string;
10+
key: string;
11+
}
12+
export interface IModernTermPickerProps extends IBasePickerProps<ITermInfoExt> {}
13+
14+
export interface ITermItemProps extends IPickerItemProps<ITermInfoExt> {
15+
/** Additional CSS class(es) to apply to the TermItem root element. */
16+
className?: string;
17+
18+
enableTermFocusInDisabledPicker?: boolean;
19+
20+
/** Call to provide customized styling that will layer on top of the variant rules. */
21+
styles?: IStyleFunctionOrObject<ITermItemStyleProps, ITermItemStyles>;
22+
23+
/** Theme provided by High-Order Component. */
24+
theme?: ITheme;
25+
termStoreInfo: ITermStoreInfo;
26+
languageTag: string;
27+
}
28+
29+
export type ITermItemStyleProps = Required<Pick<ITermItemProps, 'theme'>> &
30+
Pick<ITermItemProps, 'className' | 'selected' | 'disabled'> & {};
31+
32+
export interface ITermItemStyles {
33+
/** Root element of picked TermItem */
34+
root: IStyle;
35+
36+
/** Refers to the text element of the TermItem already picked. */
37+
text: IStyle;
38+
39+
/** Refers to the cancel action button on a picked TermItem. */
40+
close: IStyle;
41+
}
42+
43+
export interface ITermItemSuggestionProps extends React.AllHTMLAttributes<HTMLElement> {
44+
/** Additional CSS class(es) to apply to the TermItemSuggestion div element */
45+
className?: string;
46+
47+
/** Call to provide customized styling that will layer on top of the variant rules. */
48+
styles?: IStyleFunctionOrObject<ITermItemSuggestionStyleProps, ITermItemSuggestionStyles>;
49+
50+
/** Theme provided by High-Order Component. */
51+
theme?: ITheme;
52+
}
53+
54+
export type ITermItemSuggestionStyleProps = Required<Pick<ITermItemSuggestionProps, 'theme'>> &
55+
Pick<ITermItemSuggestionProps, 'className'> & {};
56+
57+
export interface ITermItemSuggestionStyles {
58+
/** Refers to the text element of the TermItemSuggestion */
59+
suggestionTextOverflow?: IStyle;
60+
}

src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,17 @@
3030
font-size: 18px;
3131
font-weight: 100;
3232
}
33+
34+
.spinnerContainer {
35+
height: 48px;
36+
line-height: 48px;
37+
display: flex;
38+
justify-content: center;
39+
align-items: center;
40+
}
41+
42+
.loadMoreContainer {
43+
height: 48px;
44+
line-height: 48px;
45+
}
3346
}

0 commit comments

Comments
 (0)