Skip to content

Commit

Permalink
Changed from tag picker to term picker
Browse files Browse the repository at this point in the history
  • Loading branch information
patrikhellgren committed Sep 24, 2021
1 parent e092701 commit d7d86a2
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 90 deletions.
159 changes: 120 additions & 39 deletions src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@ import * as React from 'react';
import { BaseComponentContext } from '@microsoft/sp-component-base';
import { Guid } from '@microsoft/sp-core-library';
import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon';
import { PrimaryButton, DefaultButton, IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
import { PrimaryButton,
DefaultButton,
IconButton,
IButtonStyles
} from 'office-ui-fabric-react/lib/Button';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { IBasePickerStyleProps, IBasePickerStyles, ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers';
import { IStackTokens, Stack } from 'office-ui-fabric-react/lib/Stack';
import { Panel,
PanelType
} from 'office-ui-fabric-react/lib/Panel';
import { IBasePickerStyleProps,
IBasePickerStyles,
ISuggestionItemProps
} from 'office-ui-fabric-react/lib/Pickers';
import { IStackTokens,
Stack
} from 'office-ui-fabric-react/lib/Stack';
import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities';
import { sp } from '@pnp/sp';
import { SPTaxonomyService } from '../../services/SPTaxonomyService';
Expand All @@ -16,52 +27,74 @@ import * as strings from 'ControlStrings';
import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle';
import { useId } from '@uifabric/react-hooks';
import { ITooltipHostStyles } from 'office-ui-fabric-react';
import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy';
import { ITermInfo,
ITermSetInfo,
ITermStoreInfo
} from '@pnp/sp/taxonomy';
import { TermItemSuggestion } from './termItem/TermItemSuggestion';
import { ModernTermPicker } from './modernTermPicker/ModernTermPicker';
import { ITermInfoExt,
ITermItemProps
} from './modernTermPicker/ModernTermPicker.types';
import { TermItem } from './termItem/TermItem';

export interface IModernTaxonomyPickerProps {
allowMultipleSelections: boolean;
allowMultipleSelections?: boolean;
termSetId: string;
anchorTermId?: string;
panelTitle: string;
label: string;
context: BaseComponentContext;
initialValues?: ITag[];
initialValues?: ITermInfo[];
disabled?: boolean;
required?: boolean;
onChange?: (newValue?: ITag[]) => void;
onChange?: (newValue?: ITermInfo[]) => void;
onRenderItem?: (itemProps: ITermItemProps) => JSX.Element;
onRenderSuggestionsItem?: (term: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>) => JSX.Element;
placeHolder?: string;
customPanelWidth?: number;
}

export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
const [taxonomyService] = React.useState(() => new SPTaxonomyService(props.context));
const [panelIsOpen, setPanelIsOpen] = React.useState(false);
const [selectedOptions, setSelectedOptions] = React.useState<ITag[]>(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []);
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITag[]>([]);
const [termStoreInfo, setTermStoreInfo] = React.useState<ITermStoreInfo>();
const [termSetInfo, setTermSetInfo] = React.useState<ITermSetInfo>();
const [anchorTermInfo, setAnchorTermInfo] = React.useState<ITermInfo>();
const [selectedOptions, setSelectedOptions] = React.useState<ITermInfoExt[]>([]);
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITermInfoExt[]>([]);
const [currentTermStoreInfo, setCurrentTermStoreInfo] = React.useState<ITermStoreInfo>();
const [currentTermSetInfo, setCurrentTermSetInfo] = React.useState<ITermSetInfo>();
const [currentAnchorTermInfo, setCurrentAnchorTermInfo] = React.useState<ITermInfo>();
const [currentLanguageTag, setCurrentLanguageTag] = React.useState<string>("");

React.useEffect(() => {
sp.setup(props.context);
taxonomyService.getTermStoreInfo()
.then((localTermStoreInfo) => {
setTermStoreInfo(localTermStoreInfo);
.then((termStoreInfo) => {
setCurrentTermStoreInfo(termStoreInfo);
setCurrentLanguageTag(props.context.pageContext.cultureInfo.currentUICultureName !== '' ?
props.context.pageContext.cultureInfo.currentUICultureName :
currentTermStoreInfo.defaultLanguageTag);
setSelectedOptions(Object.prototype.toString.call(props.initialValues) === '[object Array]' ?
props.initialValues.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfoExt;}) :
[]);
});
taxonomyService.getTermSetInfo(Guid.parse(props.termSetId))
.then((localTermSetInfo) => {
setTermSetInfo(localTermSetInfo);
.then((termSetInfo) => {
setCurrentTermSetInfo(termSetInfo);
});
if (props.anchorTermId && props.anchorTermId !== Guid.empty.toString()) {
taxonomyService.getTermById(Guid.parse(props.termSetId), props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty)
.then((localAnchorTermInfo) => {
setAnchorTermInfo(localAnchorTermInfo);
.then((anchorTermInfo) => {
setCurrentAnchorTermInfo(anchorTermInfo);
});
}
}, []);

React.useEffect(() => {
if (props.onChange) {
props.onChange(selectedOptions);
props.onChange(selectedOptions.map(termInfoExt => {
const {languageTag, termStoreInfo, ...termInfo} = termInfoExt;
return termInfo;
}));
}
}, [selectedOptions]);

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

async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise<ITag[]> {
const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : termStoreInfo.defaultLanguageTag;
async function onResolveSuggestions(filter: string, selectedItems?: ITermInfoExt[]): Promise<ITermInfoExt[]> {
if (filter === '') {
return [];
}
const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty);
const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, currentLanguageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty);
const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => {
if (!selectedItems || selectedItems.length === 0) {
return true;
}
return selectedItems.every((item) => item.key !== term.id);
return selectedItems.every((item) => item.id !== term.id);
});
const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable);
const filteredTags = filteredTermsAndAvailable.map((term) => {
const key = term.id;
let labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag));
const filteredTermsAndAvailableAsExt = filteredTermsAndAvailable.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfoExt;});
return filteredTermsAndAvailableAsExt;
}

async function onLoadParentLabel(termId: Guid): Promise<string> {
const termInfo = await taxonomyService.getTermById(Guid.parse(props.termSetId), termId);
if (termInfo.parent) {
let labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
if (labelsWithMatchingLanguageTag.length === 0) {
labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === termStoreInfo.defaultLanguageTag));
labelsWithMatchingLanguageTag = termInfo.parent.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
}
const name = labelsWithMatchingLanguageTag.filter((termLabel) => termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name;
return { key: key, name: name };
});
return filteredTags;
return labelsWithMatchingLanguageTag[0]?.name;
}
else {
let termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentLanguageTag);
if (termSetNames.length === 0) {
termSetNames = currentTermSetInfo.localizedNames.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag);
}
return termSetNames[0].name;
}
}

function onRenderSuggestionsItem(term: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>): JSX.Element {
return (
<TermItemSuggestion
onLoadParentLabel={onLoadParentLabel}
term={term}
termStoreInfo={currentTermStoreInfo}
languageTag={currentLanguageTag}
{...itemProps}
/>
);
}

function onRenderItem(itemProps: ITermItemProps): JSX.Element {
let labels = itemProps.item.labels.filter((name) => name.languageTag === currentLanguageTag && name.isDefault);
if (labels.length === 0) {
labels = itemProps.item.labels.filter((name) => name.languageTag === currentTermStoreInfo.defaultLanguageTag && name.isDefault);
}

return labels.length > 0 ? (
<TermItem languageTag={currentLanguageTag} termStoreInfo={currentTermStoreInfo} {...itemProps}>{labels[0].name}</TermItem>
) : null;
}

function getTextFromItem(termInfo: ITermInfoExt): string {
let labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentLanguageTag));
if (labelsWithMatchingLanguageTag.length === 0) {
labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag));
}
return labelsWithMatchingLanguageTag[0]?.name;
}

const calloutProps = { gapSpace: 0 };
Expand All @@ -119,22 +192,25 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
{props.label && <Label required={props.required}>{props.label}</Label>}
<div className={styles.termField}>
<div className={styles.termFieldInput}>
<TagPicker
<ModernTermPicker
removeButtonAriaLabel={strings.ModernTaxonomyPickerRemoveButtonText}
onResolveSuggestions={onResolveSuggestions}
itemLimit={props.allowMultipleSelections ? undefined : 1}
selectedItems={selectedOptions}
disabled={props.disabled}
styles={tagPickerStyles}
onChange={(itms?: ITag[]) => {
onChange={(itms?: ITermInfoExt[]) => {
setSelectedOptions(itms || []);
setSelectedPanelOptions(itms || []);
}}
getTextFromItem={(tag: ITag) => tag.name}
getTextFromItem={getTextFromItem}
pickerSuggestionsProps={{noResultsFoundText: strings.ModernTaxonomyPickerNoResultsFound}}
inputProps={{
'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder,
placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder
}}
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
onRenderItem={props.onRenderItem ?? onRenderItem}
/>
</div>
<div className={styles.termFieldButton}>
Expand All @@ -155,7 +231,8 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
closeButtonAriaLabel={strings.ModernTaxonomyPickerPanelCloseButtonText}
onDismiss={onClosePanel}
isLightDismiss={true}
type={PanelType.medium}
type={props.customPanelWidth ? PanelType.custom : PanelType.medium}
customWidth={props.customPanelWidth ? `${props.customPanelWidth}px` : undefined}
headerText={props.panelTitle}
onRenderFooterContent={() => {
const horizontalGapStackTokens: IStackTokens = {
Expand All @@ -176,15 +253,19 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
allowMultipleSelections={props.allowMultipleSelections}
onResolveSuggestions={onResolveSuggestions}
onLoadMoreData={taxonomyService.getTerms}
anchorTermInfo={anchorTermInfo}
termSetInfo={termSetInfo}
termStoreInfo={termStoreInfo}
anchorTermInfo={currentAnchorTermInfo}
termSetInfo={currentTermSetInfo}
termStoreInfo={currentTermStoreInfo}
context={props.context}
termSetId={Guid.parse(props.termSetId)}
pageSize={50}
selectedPanelOptions={selectedPanelOptions}
setSelectedPanelOptions={setSelectedPanelOptions}
placeHolder={props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder}
onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem}
onRenderItem={props.onRenderItem ?? onRenderItem}
getTextFromItem={getTextFromItem}
languageTag={currentLanguageTag}
/>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions src/controls/modernTaxonomyPicker/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ModernTaxonomyPicker';
export * from './termItem/TermItem';
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from "react";
import { BasePicker } from "office-ui-fabric-react/lib/components/pickers/BasePicker";
import { IModernTermPickerProps,
ITermInfoExt,
ITermItemProps
} from "./ModernTermPicker.types";
import { TermItem } from "../termItem/TermItem";
import { TermItemSuggestion } from "../termItem/TermItemSuggestion";
import { IBasePickerStyleProps,
IBasePickerStyles
} from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
import { getStyles } from "office-ui-fabric-react/lib/components/pickers/BasePicker.styles";
import { initializeComponentRef,
styled
} from "office-ui-fabric-react/lib/Utilities";
import { ISuggestionItemProps } from "office-ui-fabric-react/lib/components/pickers/Suggestions/SuggestionsItem.types";
import { Guid } from "@microsoft/sp-core-library";

export class ModernTermPickerBase extends BasePicker<ITermInfoExt, IModernTermPickerProps> {
public static defaultProps = {
onRenderItem: (props: ITermItemProps) => {
let labels = props.item.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault);
if (labels.length === 0) {
labels = props.item.labels.filter((name) => name.languageTag === props.termStoreInfo?.defaultLanguageTag && name.isDefault);
}

return labels.length > 0 ? (
<TermItem {...props}>{labels[0].name}</TermItem>
) : null;
},
onRenderSuggestionsItem: (props: ITermInfoExt, itemProps: ISuggestionItemProps<ITermInfoExt>) => {
const onLoadParentLabel = async (termId: Guid): Promise<string> => {
return Promise.resolve("");
};
return <TermItemSuggestion term={props} languageTag={props.languageTag} onLoadParentLabel={onLoadParentLabel} termStoreInfo={props.termStoreInfo} {...itemProps} />;
},
};

constructor(props: IModernTermPickerProps) {
super(props);
initializeComponentRef(this);
}
}

export const ModernTermPicker = styled<IModernTermPickerProps, IBasePickerStyleProps, IBasePickerStyles>(
ModernTermPickerBase,
getStyles,
undefined,
{
scope: 'ModernTermPicker',
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ITermInfo, ITermStoreInfo } from "@pnp/sp/taxonomy";
import { IBasePickerProps } from "office-ui-fabric-react/lib/components/pickers/BasePicker.types";
import { IPickerItemProps } from "office-ui-fabric-react/lib/components/pickers/PickerItem.types";
import { IStyle, ITheme } from "office-ui-fabric-react/lib/Styling";
import { IStyleFunctionOrObject } from "office-ui-fabric-react/lib/Utilities";

export interface ITermInfoExt extends ITermInfo {
termStoreInfo: ITermStoreInfo;
languageTag: string;
key: string;
}
export interface IModernTermPickerProps extends IBasePickerProps<ITermInfoExt> {}

export interface ITermItemProps extends IPickerItemProps<ITermInfoExt> {
/** Additional CSS class(es) to apply to the TermItem root element. */
className?: string;

enableTermFocusInDisabledPicker?: boolean;

/** Call to provide customized styling that will layer on top of the variant rules. */
styles?: IStyleFunctionOrObject<ITermItemStyleProps, ITermItemStyles>;

/** Theme provided by High-Order Component. */
theme?: ITheme;
termStoreInfo: ITermStoreInfo;
languageTag: string;
}

export type ITermItemStyleProps = Required<Pick<ITermItemProps, 'theme'>> &
Pick<ITermItemProps, 'className' | 'selected' | 'disabled'> & {};

export interface ITermItemStyles {
/** Root element of picked TermItem */
root: IStyle;

/** Refers to the text element of the TermItem already picked. */
text: IStyle;

/** Refers to the cancel action button on a picked TermItem. */
close: IStyle;
}

export interface ITermItemSuggestionProps extends React.AllHTMLAttributes<HTMLElement> {
/** Additional CSS class(es) to apply to the TermItemSuggestion div element */
className?: string;

/** Call to provide customized styling that will layer on top of the variant rules. */
styles?: IStyleFunctionOrObject<ITermItemSuggestionStyleProps, ITermItemSuggestionStyles>;

/** Theme provided by High-Order Component. */
theme?: ITheme;
}

export type ITermItemSuggestionStyleProps = Required<Pick<ITermItemSuggestionProps, 'theme'>> &
Pick<ITermItemSuggestionProps, 'className'> & {};

export interface ITermItemSuggestionStyles {
/** Refers to the text element of the TermItemSuggestion */
suggestionTextOverflow?: IStyle;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,17 @@
font-size: 18px;
font-weight: 100;
}

.spinnerContainer {
height: 48px;
line-height: 48px;
display: flex;
justify-content: center;
align-items: center;
}

.loadMoreContainer {
height: 48px;
line-height: 48px;
}
}
Loading

0 comments on commit d7d86a2

Please sign in to comment.