From 8e9fdc67e35a869c9b13df26dda4af91a7ccfdbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Lindstr=C3=B6m?= Date: Mon, 7 Jun 2021 18:49:28 +0200 Subject: [PATCH 01/19] Add modern taxonomy picker --- src/ModernTaxonomyPicker.ts | 1 + .../ModernTaxonomyPicker.module.scss | 167 +++++++++ .../ModernTaxonomyPicker.tsx | 187 ++++++++++ src/controls/modernTaxonomyPicker/index.ts | 1 + .../taxonomyForm/TaxonomyForm.module.scss | 104 ++++++ .../taxonomyForm/TaxonomyForm.tsx | 333 ++++++++++++++++++ .../taxonomyForm/index.ts | 1 + src/services/SPTaxonomyService.ts | 87 +++++ 8 files changed, 881 insertions(+) create mode 100644 src/ModernTaxonomyPicker.ts create mode 100644 src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss create mode 100644 src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx create mode 100644 src/controls/modernTaxonomyPicker/index.ts create mode 100644 src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss create mode 100644 src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx create mode 100644 src/controls/modernTaxonomyPicker/taxonomyForm/index.ts create mode 100644 src/services/SPTaxonomyService.ts diff --git a/src/ModernTaxonomyPicker.ts b/src/ModernTaxonomyPicker.ts new file mode 100644 index 000000000..dbf559142 --- /dev/null +++ b/src/ModernTaxonomyPicker.ts @@ -0,0 +1 @@ +export * from './controls/modernTaxonomyPicker'; diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss new file mode 100644 index 000000000..d4830beae --- /dev/null +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss @@ -0,0 +1,167 @@ +.modernTaxonomyPicker { + +.contextualMenu { + display: inline-block +} + +.listItem { + min-height: 36px; + line-height: 36px; + cursor: pointer; + + >div { + display: inline-block; + margin-right: 10px; + } + + img { + margin-right: 5px; + vertical-align: middle; + } +} + +.termField { + align-items: center; + border-spacing: 0; + display: flex; + width: 100%; + + .termFieldInput { + flex-grow: 1; + } + + .termFieldButton { + text-align: center; + width: 42px; + } + + input[type="text"] { + cursor: pointer; + opacity: 0.8; + width: 100%; + } +} + +.termset { + cursor: pointer; + 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; + + .termEnabled, + .termDisabled, + .termNoTagging { + background-repeat: no-repeat; + background-position: 30px center; + } + + .termEnabled { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACzSURBVDhPY2AYNKCoqIgTiOcD8X8S8F6wB4Aa1IH4akNDw+mPHz++/E8EuHTp0jmQRSDNCcXFxa/XrVt3gAh9KEpgBvx/9OjRLVI1g9TDDYBp3rlz5//Kysr/IJoYgGEASPPatWsbQDQxAMOAbdu2gZ0FookBcAOePHlyhxgN6GqQY+Hdhg0bDpJqCNgAaDrQAnJuNDY2nvr06dMbYgw6e/bsabgBUEN4yEiJ2wdNViLfIQC3sTh2vtJcswAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTerm.png + } + + .termDisabled { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFMSURBVDhPzZPNSsNAFIULQqEQEPoSQqAg7gqC0HdwXxAElyYgBBGSZ+gDdOUyIauULATdKrgIKCKuQjbiDySCkNV4TrkzpJW2cefAxwwzc86de2em0/k3zXGcHpgC9Qeu5glAsAMefN+/K8vyVbVoWZbdMxDFY9d136Ioum6hW9iiDVSe588rxDXmJ+AAdAWOOVcbAy1O01R5nqfYoxVglyk+Hu7Z4FiwOcc1GBRMwQSnOAxDHz0jDyCwwCVQS3DO0gU0BkmSzG8A/UQiz7DxC5yLGQ1PwDeYGYOiKF6WarCPDUOJeor+A4z0m8P4SNaG+hY+4zi+aZh0scEBNeB41DTBuCcGjj6FjaM/BUFwW1XVO6vdMNiSdIzJLwN5TJZ+iSLQKYwbR9cmZyaFdX+JhZIiMue+cLFQxA0G22uusd/6I8OEb4LXRwZN4Q+3Ys8Mb9+nRgAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTermDeprecated.png + } + + .termNoTagging { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZBJREFUeNqkU0trwkAQnhURBBFPKghekuJN7KW9pOCj0Agl9/4uT/6BHgXpxQfxYKltKEYhV3tXFF/4RrOdXYgkxpz8YA47O983O9+whFIKt8DvddHv9wej0UjY7/dwPB55sGaJRKImSVLxXMiSl9Hr9fRWq0WvoVKpUFVVv6xaF1nX9R+LvFqteKAgrVarVNM0fi6Xy9Sq99mfjeTv2Wz2mMvlYL1en/OGYYCiKKTT6fAzG8flQbfb1ReLxX0+n4fJZOLwQxRFKJVKNJPJwHa7dZuI5I/lcsnJrHMwGHQUpdNpHp5bGI/Hr7IsA3Pc7/dcDEynU5jP5xAIBJwCpmmeN+IF9IaT0VA20p+V5yaGQqF2s9mE3W4Hh8PBFXZyLBbTstmsaAkQqyuu7nOz2UiCIEAkEoFwOMzz6A0n44YgHo8bhULBYQaxP7vRaLyjyFsqleIiDLbOAyTfXY5GLueu1+s1XNULE2Fg5Gg0qj4jrnlDrhmHIu3hcPh0Op0gmUz+IvfBy1xy62/0wY34F2AAKtctO7g/KgIAAAAASUVORK5CYII='); // /_layouts/15/Images/EMMTermDisabled.png + } + + label>span { + padding-left: 25px; + } +} + +.actions { + button:first-child { + margin-right: 15px; + } +} + +.termBasePicker +{ + background-color: #fff; +} + .termSuggestion + { + min-height: 40px; + width: 100%; + text-align: left; + cursor: pointer; + + + .termSuggestionSubTitle + { + font-size: 12px; + color: #666666; + } + + } + + .pickedTermRoot + { + position: relative; + outline: transparent; + box-sizing: content-box; + flex-shrink: 1; + background: #f4f4f4; + margin: 2px; + height: 26px; + line-height: 26px; + cursor: default; + display: flex; + flex-wrap: nowrap; + max-width: 300px; + + .pickedTermText + { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 30px; + margin: 0 8px; + } + .pickedTermCloseIcon + { + cursor: pointer; + color: #666666; + font-size: 12px; + display: inline-block; + text-align: center; + vertical-align: top; + width: 30px; + height: 100%; + -ms-flex-negative: 0; + flex-shrink: 0; + } + } + + .errorMessage { + font-size: 12px; + font-weight: 400; + color: #a80000; + margin: 0; + padding-top: 5px; + display: flex; + align-items: center; + } + + .errorIcon { + font-size: 14px; + margin-right: 5px; + } + +} diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx new file mode 100644 index 000000000..3e36d6c8c --- /dev/null +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -0,0 +1,187 @@ +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 } 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 { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers'; +import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; +import { IStackTokens, Stack } from 'office-ui-fabric-react/lib/Stack'; +import { sp } from '@pnp/sp'; +import { ITermInfo } from '@pnp/sp/taxonomy'; +import { SPTaxonomyService } from '../../services/SPTaxonomyService'; +import FieldErrorMessage from '../errorMessage/ErrorMessage'; +import { TaxonomyForm } from './taxonomyForm'; +import styles from './ModernTaxonomyPicker.module.scss'; +import * as strings from 'ControlStrings'; + +// TODO: remove/replace interface IPickerTerm +export interface IPickerTerm { + name: string; + key: string; + path: string; + termSet: string; + termSetName?: string; +} + +// TODO: remove/replace interface IPickerTerms +export interface IPickerTerms extends Array { } + +export interface IModernTaxonomyPickerProps { + allowMultipleSelections: boolean; + termSetId: string; + anchorTermId?: string; + panelTitle: string; + label: string; + context: BaseComponentContext; + initialValues?: ITag[]; + errorMessage?: string; // TODO: is this needed? + disabled?: boolean; + required?: boolean; +} + +export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { + const [termsService] = React.useState(() => new SPTaxonomyService(props.context)); + const [terms, setTerms] = React.useState([]); + const [errorMessage, setErrorMessage] = React.useState(props.errorMessage); + const [internalErrorMessage, setInternalErrorMessage] = React.useState(); + const [panelIsOpen, setPanelIsOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); // was called loaded + const [selectedOptions, setSelectedOptions] = React.useState([]); + const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); + + const invalidTerm = React.useRef(null); + + React.useEffect(() => { + sp.setup(props.context); + }, []); + + React.useEffect(() => { + setSelectedOptions(props.initialValues || []); + }, [props.initialValues]); + + React.useEffect(() => { + setErrorMessage(props.errorMessage); + }, [props.errorMessage]); + + async function onOpenPanel(): Promise { + if (props.disabled === true) { + return; + } + setLoading(true); + const siteUrl = props.context.pageContext.site.absoluteUrl; + const newTerms = await termsService.getTerms(Guid.parse(props.termSetId), Guid.empty, '', true, 50); + setTerms(newTerms.value); + setLoading(false); + setPanelIsOpen(true); + } + + function onClosePanel(): void { + setLoading(false); + setPanelIsOpen(false); + } + + function onSave(): void { + setSelectedOptions([...selectedPanelOptions]); + onClosePanel(); + } + + async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise { + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + if (filter === '') { + return []; + } + const filteredTerms = await termsService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : undefined); + const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => { + if (!selectedItems || selectedItems.length === 0) { + return true; + } + for (const selectedItem of selectedItems) { + return selectedItem.key !== 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; + const name = term.labels.filter((termLabel) => (languageTag === '' || termLabel.languageTag === languageTag) && + termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name; + return { key: key, name: name }; + }); + return filteredTags; + } + + const { label, disabled, allowMultipleSelections, panelTitle, required } = props; + return ( +
+ {label && } +
+
+ { + setSelectedOptions(itms || []); + setSelectedPanelOptions(itms || []); + }} + getTextFromItem={(tag: ITag, currentValue?: string) => tag.name} + inputProps={{ + 'aria-label': 'Tag Picker', + placeholder: 'Ange en term som du vill tagga' + }} + /> +
+
+ +
+
+ + + + { + const horizontalGapStackTokens: IStackTokens = { + childrenGap: 10, + }; + return ( + + + + + ); + }}> + + { + /* Show spinner in the panel while retrieving terms */ + loading === true ? : '' + } + { + loading === false && props.termSetId && ( +
+ +
+ ) + } +
+
+ ); +} diff --git a/src/controls/modernTaxonomyPicker/index.ts b/src/controls/modernTaxonomyPicker/index.ts new file mode 100644 index 000000000..bfa130c40 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/index.ts @@ -0,0 +1 @@ +export * from './ModernTaxonomyPicker'; diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss new file mode 100644 index 000000000..6801a69a4 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss @@ -0,0 +1,104 @@ +@import '~office-ui-fabric-react/dist/sass/References.scss'; + +.taxonomyForm { + .container { + max-width: 700px; + margin: 0px auto; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + } + + .row { + @include ms-Grid-row; + // @include ms-fontColor-white; + // background-color: $ms-color-themeDark; + padding: 20px; + } + + .column { + @include ms-Grid-col; + @include ms-lg10; + @include ms-xl8; + @include ms-xlPush2; + @include ms-lgPush1; + } + + .title { + @include ms-font-xl; + // @include ms-fontColor-white; + } + + .subTitle { + @include ms-font-l; + // @include ms-fontColor-white; + } + + .description { + @include ms-font-l; + // @include ms-fontColor-white; + } + + .button { + // Our button + text-decoration: none; + height: 32px; + + // Primary Button + min-width: 80px; + background-color: $ms-color-themePrimary; + border-color: $ms-color-themePrimary; + color: $ms-color-white; + + // Basic Button + outline: transparent; + position: relative; + font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif; + -webkit-font-smoothing: antialiased; + font-size: $ms-font-size-m; + font-weight: $ms-font-weight-regular; + border-width: 0; + text-align: center; + cursor: pointer; + display: inline-block; + padding: 0 16px; + + .label { + font-weight: $ms-font-weight-semibold; + font-size: $ms-font-size-m; + height: 32px; + line-height: 32px; + margin: 0 4px; + vertical-align: top; + display: inline-block; + } + } + + .choiceOption { + color: "[theme: bodyText, default: #323130]"; + padding-left: 26px; + } + + .selectedChoiceOption { + font-weight: bold; + } + + .checkbox { + color: "[theme: bodyText, default: #323130]"; + margin-left: 4px; + } + + .selectedCheckbox { + font-weight: bold; + } + + .taxonomyTreeSelector { + border-bottom-color: blue; + border-bottom-style: solid; + border-bottom-width: 1px; + margin-bottom: 30px; + } + + .taxonomyTreeLabel { + font-size: 18px; + font-weight: 100; + } +} diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx new file mode 100644 index 000000000..e56103790 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx @@ -0,0 +1,333 @@ +import * as React from 'react'; +import styles from './TaxonomyForm.module.scss'; +import { Checkbox, ChoiceGroup, classNamesFunction, DetailsRow, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionProps, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IChoiceGroupStyleProps, IChoiceGroupStyles, IColumn, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderCheckboxProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react'; +import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy'; +import { Guid } from '@microsoft/sp-core-library'; +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { css } from '@uifabric/utilities/lib/css'; + +export interface ITaxonomyFormProps { + context: BaseComponentContext; + allowMultipleSelections: boolean; + terms: ITermInfo[]; + termSetId: Guid; + pageSize: number; + selectedPanelOptions: ITag[]; + setSelectedPanelOptions: React.Dispatch>; + onResolveSuggestions: (filter: string, selectedItems?: ITag[]) => ITag[] | PromiseLike; + onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; + getTermSetInfo: (termSetId: Guid) => Promise; +} + +export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { + const groupedListRef = React.useRef(); + + const [groupsLoading, setGroupsLoading] = React.useState([]); + const [groups, setGroups] = React.useState([]); + + + React.useEffect(() => { + setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, props.termSetId.toString()]); + + props.getTermSetInfo(props.termSetId) + .then((termSetInfo) => { + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + + const termSetName = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag)[0].name; + const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: false }; + setGroups([rootGroup]); + props.onLoadMoreData(props.termSetId, Guid.empty, '', true) + .then((terms) => { + const grps: IGroup[] = terms.value.map(term => { + const g: IGroup = { + name: term.labels?.[0].name, // TODO: fix this by looking up correct language + key: term.id, + startIndex: -1, + count: 50, + level: 1, + isCollapsed: true, + data: { skiptoken: '', term: term }, + hasMoreData: term.childrenCount > 0, + }; + if (g.hasMoreData) { + g.children = []; + } + return g; + }); + rootGroup.children = grps; + rootGroup.data.skiptoken = terms.skiptoken; + rootGroup.hasMoreData = terms.skiptoken !== ''; + setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== props.termSetId.toString())); + setGroups([rootGroup]); + }); + + }); + }, []); + + const onToggleCollapse = (group: IGroup): void => { + if (group.isCollapsed === true) { + setGroups((prevGroups) => { + const recurseGroups = (currentGroup) => { + if (currentGroup.key === group.key) { + currentGroup.isCollapsed = false; + } + if (currentGroup.children?.length > 0) { + for (const child of currentGroup.children) { + recurseGroups(child); + } + } + }; + let newGroupsState: IGroup[] = []; + for (const prevGroup of prevGroups) { + recurseGroups(prevGroup); + newGroupsState.push(prevGroup); + } + + return newGroupsState; + }); + + if (group.children && group.children.length === 0) { + setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, group.key]); + group.data.isLoading = true; + + props.onLoadMoreData(props.termSetId, Guid.parse(group.key), '', true) + .then((terms) => { + const grps: IGroup[] = terms.value.map(term => { + const g: IGroup = { + name: term.labels?.[0].name, // TODO: fix this by looking up correct language + key: term.id, + startIndex: -1, + count: 50, + level: group.level + 1, + isCollapsed: true, + data: { skiptoken: '', term: term }, + hasMoreData: term.childrenCount > 0, + }; + if (g.hasMoreData) { + g.children = []; + } + return g; + }); + group.children = grps; + group.data.skiptoken = terms.skiptoken; + group.hasMoreData = terms.skiptoken !== ''; + setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== group.key)); + }); + } + } + else { + setGroups((prevGroups) => { + const recurseGroups = (currentGroup) => { + if (currentGroup.key === group.key) { + currentGroup.isCollapsed = true; + } + if (currentGroup.children?.length > 0) { + for (const child of currentGroup.children) { + recurseGroups(child); + } + } + }; + let newGroupsState: IGroup[] = []; + for (const prevGroup of prevGroups) { + recurseGroups(prevGroup); + newGroupsState.push(prevGroup); + } + + return newGroupsState; + }); + + } + }; + + const onChoiceChange = (ev?: React.FormEvent, option?: IChoiceGroupOption): void => { + props.setSelectedPanelOptions([{ key: option.key, name: option.text }]); + }; + + const onCheckboxChange = (ev?: React.FormEvent, checked?: boolean, tag?: ITag): void => { + if (checked) { + props.setSelectedPanelOptions((prevOptions) => [...prevOptions, tag]); + } + else { + props.setSelectedPanelOptions((prevOptions) => prevOptions.filter((value) => value.key !== tag.key)); + } + }; + + const onRenderTitle = (groupHeaderProps: IGroupHeaderProps) => { + if (groupHeaderProps.group.level === 0) { + return ( + + ); + } + if (props.allowMultipleSelections) { + const isSelected = props.selectedPanelOptions.some(value => value.key === groupHeaderProps.group.key); + const selectedStyles: IStyleFunctionOrObject = isSelected ? { label: { fontWeight: 'bold' } } : { label: { fontWeight: 'normal' } }; + + return ( + , checked?: boolean) => + onCheckboxChange(ev, checked, { name: groupHeaderProps.group.name, key: groupHeaderProps.group.key })} + checked={isSelected} + styles={selectedStyles} + disabled={groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false} + onRenderLabel={(p) => + {p.label} + } + /> + ); + } + else { + const isSelected = props.selectedPanelOptions?.[0]?.key === groupHeaderProps.group.key; + const selectedStyle: IStyleFunctionOrObject = isSelected ? { choiceFieldWrapper: { fontWeight: 'bold' } } : { choiceFieldWrapper: { fontWeight: 'normal' } }; + const getClassNames = classNamesFunction(); + + const classNames = getClassNames(selectedStyle!, { + theme: undefined, + hasIcon: false, + hasImage: false, + checked: false, + disabled: false, + imageIsLarge: false, + imageSize: undefined, + focused: false, + }); + const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false; + const options: IChoiceGroupOption[] = [{ key: groupHeaderProps.group.key, text: groupHeaderProps.group.name, styles: selectedStyle, onRenderLabel: (p) => }]; + return ( + + ); + } + }; + + const onRenderHeader = (headerProps: IGroupHeaderProps): JSX.Element => { + const headerCountStyle = { display: 'none' }; + const checkButtonStyle = { display: 'none' }; + const expandStyle = { visibility: 'hidden' }; + const groupHeaderStyles: IStyleFunctionOrObject = { + expand: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null, + expandIsCollapsed: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null, + check: checkButtonStyle, + headerCount: headerCountStyle, + groupHeaderContainer: { height: 36, paddingTop: 3, paddingBottom: 3, paddingLeft: 3, paddingRight: 3, alignItems: 'center', }, + root: { height: 42 } + }; + + return ( + + ); + }; + + const onRenderFooter = (footerProps: IGroupFooterProps): JSX.Element => { + if ((footerProps.group.hasMoreData || footerProps.group.children && footerProps.group.children.length === 0) && !footerProps.group.isCollapsed) { + if (groupsLoading.some(value => value === footerProps.group.key)) { + const spinnerStyles: IStyleFunctionOrObject = { circle: { verticalAlign: 'middle' } }; + return ( +
+ +
+ ); + } + const linkStyles: IStyleFunctionOrObject = { root: { fontSize: '14px', paddingLeft: (footerProps.groupLevel + 1) * 20 + 62 } }; + return ( +
+ { + props.onLoadMoreData(props.termSetId, footerProps.group.key === props.termSetId.toString() ? Guid.empty : Guid.parse(footerProps.group.key), footerProps.group.data.skiptoken, true) + .then((terms) => { + const grps: IGroup[] = terms.value.map(term => { + const g: IGroup = { + name: term.labels?.[0].name, // TODO: fix this by looking up correct language + key: term.id, + startIndex: -1, + count: 50, + level: footerProps.group.level + 1, + isCollapsed: true, + data: { skiptoken: '', term: term }, + hasMoreData: term.childrenCount > 0, + }; + if (g.hasMoreData) { + g.children = []; + } + return g; + }); + footerProps.group.children = [...footerProps.group.children, ...grps]; + footerProps.group.data.skiptoken = terms.skiptoken; + footerProps.group.hasMoreData = terms.skiptoken !== ''; + setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== footerProps.group.key)); + }); + }} + styles={linkStyles}> + Load more... + +
+ ); + } + return null; + }; + + const onRenderShowAll: IRenderFunction = () => { + return null; + }; + + const groupProps: IGroupRenderProps = { + onRenderFooter: onRenderFooter, + onRenderHeader: onRenderHeader, + showEmptyGroups: true, + onRenderShowAll: onRenderShowAll, + }; + + function getTagText(tag: ITag, currentValue?: string) { + return tag.name; + } + + const onPickerChange = (itms?: ITag[]): void => { + props.setSelectedPanelOptions(itms || []); + }; + + const tagPickerStyles: IStyleFunctionOrObject = { text: { borderStyle: 'none', borderWidth: '0px' } }; + + return ( +
+
+
+ +
+
+ +
+ ) => false} + /> +
+
+ ); +} + diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/index.ts b/src/controls/modernTaxonomyPicker/taxonomyForm/index.ts new file mode 100644 index 000000000..be10cc5ba --- /dev/null +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/index.ts @@ -0,0 +1 @@ +export * from './TaxonomyForm'; diff --git a/src/services/SPTaxonomyService.ts b/src/services/SPTaxonomyService.ts new file mode 100644 index 000000000..168266da1 --- /dev/null +++ b/src/services/SPTaxonomyService.ts @@ -0,0 +1,87 @@ +import { BaseComponentContext } from '@microsoft/sp-component-base'; +import { Guid } from '@microsoft/sp-core-library'; +import { LambdaParser } from '@pnp/odata/parsers'; +import { SharePointQueryableCollection, sp } from '@pnp/sp'; +import '@pnp/sp/taxonomy'; +import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy'; + +export interface ODataCollection { + '@odata.context': string; + '@odata.nextLink'?:string; + value: T[]; +} + +const EmptyODataCollection = { '@odata.context': '', value: [] }; + +export class SPTaxonomyService { + + /** + * Service constructor + */ + constructor(private context: BaseComponentContext) { + + } + + public async getTerms(termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize: number = 50): Promise<{ value: ITermInfo[], skiptoken: string }> { + try { + const parser = new LambdaParser(async (r: Response) => { + const json = await r.json(); + let newSkiptoken=''; + if(json['@odata.nextLink']) { + var urlParams = new URLSearchParams(json['@odata.nextLink'].split('?')[1]); + if(urlParams.has('$skiptoken')) { + newSkiptoken = urlParams.get('$skiptoken'); + } + } + return { value: json.value, skiptoken: newSkiptoken }; + }); + + let legacyChildrenUrlAndQuery = ''; + if (parentTermId && parentTermId !== Guid.empty) { + legacyChildrenUrlAndQuery = sp.termStore.sets.getById(termSetId.toString()).terms.getById(parentTermId.toString()).concat('/getLegacyChildren').toUrl(); + } + else { + legacyChildrenUrlAndQuery = sp.termStore.sets.getById(termSetId.toString()).concat('/getLegacyChildren').toUrl(); + } + let legacyChildrenQueryable = SharePointQueryableCollection(legacyChildrenUrlAndQuery).top(pageSize).usingParser(parser); + if (hideDeprecatedTerms) { + legacyChildrenQueryable = legacyChildrenQueryable.filter('isDeprecated eq false'); + } + if (skiptoken && skiptoken !== '') { + legacyChildrenQueryable.query.set('$skiptoken', skiptoken); + } + const termsResult = await legacyChildrenQueryable() as {value: ITermInfo[], skiptoken: string}; + return termsResult; + } catch (error) { + return { value: [], skiptoken: '' }; + } + } + + public async getTermsByIds(termSetId: Guid, termIds: string[]): Promise { + const batch = sp.createBatch(); + const results = []; + termIds.forEach(termId => { + sp.termStore.sets.getById(termSetId.toString()).terms.getById(termId).inBatch(batch)() + .then(term => results.push(term)) + .catch(reason => console.warn(`Error retreiving Term ID ${termId}: ${reason}`)); + }); + await batch.execute(); + return results; + } + + public async searchTerm(termSetId: Guid, label: string, languageTag: string, parentTermId?: Guid, stringMatchId: string = '0', pageSize: number = 50): Promise { + try { + const searchTermUrl = sp.termStore.concat(`/searchTerm(label='${label}',setId='${termSetId}',languageTag='${languageTag}',stringMatchId='${stringMatchId}'${parentTermId ? `,parentTermId='${parentTermId}'` : ''})`).toUrl(); + const searchTermQuery = SharePointQueryableCollection(searchTermUrl).top(pageSize); + const filteredTerms = await searchTermQuery(); + return filteredTerms; + } catch (error) { + return []; + } + } + + public async getTermSetInfo(termSetId: Guid): Promise { + const tsInfo = await sp.termStore.sets.getById(termSetId.toString()).get(); + return tsInfo; + } +} From 105f0e22a799d76588053aa50c240bcbae2533d6 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 8 Jun 2021 14:42:36 +0200 Subject: [PATCH 02/19] Added translations, tooltip, placeholder, fix for loading terms --- .../ModernTaxonomyPicker.tsx | 79 ++++++++++--------- .../taxonomyForm/TaxonomyForm.tsx | 69 ++++++++-------- src/index.ts | 1 + src/loc/en-us.ts | 10 ++- src/loc/mystrings.d.ts | 9 +++ src/loc/sv-se.ts | 11 ++- 6 files changed, 103 insertions(+), 76 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 3e36d6c8c..05876812d 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -2,7 +2,7 @@ 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 } 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 { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers'; @@ -15,19 +15,9 @@ import FieldErrorMessage from '../errorMessage/ErrorMessage'; import { TaxonomyForm } from './taxonomyForm'; import styles from './ModernTaxonomyPicker.module.scss'; import * as strings from 'ControlStrings'; - -// TODO: remove/replace interface IPickerTerm -export interface IPickerTerm { - name: string; - key: string; - path: string; - termSet: string; - termSetName?: string; -} - -// TODO: remove/replace interface IPickerTerms -export interface IPickerTerms extends Array { } - +import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle'; +import { useId } from '@uifabric/react-hooks'; +import { ITooltipHostStyles } from 'office-ui-fabric-react'; export interface IModernTaxonomyPickerProps { allowMultipleSelections: boolean; termSetId: string; @@ -39,15 +29,15 @@ export interface IModernTaxonomyPickerProps { errorMessage?: string; // TODO: is this needed? disabled?: boolean; required?: boolean; + onChange?: (newValue?: ITag[]) => void; + placeHolder?: string; } export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const [termsService] = React.useState(() => new SPTaxonomyService(props.context)); - const [terms, setTerms] = React.useState([]); const [errorMessage, setErrorMessage] = React.useState(props.errorMessage); const [internalErrorMessage, setInternalErrorMessage] = React.useState(); const [panelIsOpen, setPanelIsOpen] = React.useState(false); - const [loading, setLoading] = React.useState(false); // was called loaded const [selectedOptions, setSelectedOptions] = React.useState([]); const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); @@ -58,31 +48,38 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { }, []); React.useEffect(() => { - setSelectedOptions(props.initialValues || []); + if(Object.prototype.toString.call(props.initialValues) === '[object Array]' ) { + setSelectedOptions(props.initialValues); + } + else { + setSelectedOptions([]); + } }, [props.initialValues]); React.useEffect(() => { setErrorMessage(props.errorMessage); }, [props.errorMessage]); + React.useEffect(() => { + if (props.onChange) { + props.onChange(selectedOptions); + } + }, [selectedOptions]); + async function onOpenPanel(): Promise { if (props.disabled === true) { return; } - setLoading(true); - const siteUrl = props.context.pageContext.site.absoluteUrl; - const newTerms = await termsService.getTerms(Guid.parse(props.termSetId), Guid.empty, '', true, 50); - setTerms(newTerms.value); - setLoading(false); + setSelectedPanelOptions(selectedOptions); setPanelIsOpen(true); } function onClosePanel(): void { - setLoading(false); + setSelectedPanelOptions([]); setPanelIsOpen(false); } - function onSave(): void { + function onApply(): void { setSelectedOptions([...selectedPanelOptions]); onClosePanel(); } @@ -111,30 +108,42 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { return filteredTags; } - const { label, disabled, allowMultipleSelections, panelTitle, required } = props; + const { label, disabled, allowMultipleSelections, panelTitle, required, placeHolder } = props; + const calloutProps = { gapSpace: 0 }; + const tooltipId = useId('tooltip'); + const hostStyles: Partial = { root: { display: 'inline-block' } }; + return (
{label && }
{ setSelectedOptions(itms || []); setSelectedPanelOptions(itms || []); }} getTextFromItem={(tag: ITag, currentValue?: string) => tag.name} inputProps={{ - 'aria-label': 'Tag Picker', - placeholder: 'Ange en term som du vill tagga' + 'aria-label': placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder, + placeholder: placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder }} />
- + + +
@@ -153,22 +162,17 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { }; return ( - - + + ); }}> { - /* Show spinner in the panel while retrieving terms */ - loading === true ? : '' - } - { - loading === false && props.termSetId && ( + props.termSetId && (
) diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx index e56103790..a820f3d3b 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import styles from './TaxonomyForm.module.scss'; -import { Checkbox, ChoiceGroup, classNamesFunction, DetailsRow, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionProps, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IChoiceGroupStyleProps, IChoiceGroupStyles, IColumn, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderCheckboxProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react'; +import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react'; import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy'; import { Guid } from '@microsoft/sp-core-library'; import { BaseComponentContext } from '@microsoft/sp-component-base'; import { css } from '@uifabric/utilities/lib/css'; +import * as strings from 'ControlStrings'; export interface ITaxonomyFormProps { context: BaseComponentContext; allowMultipleSelections: boolean; - terms: ITermInfo[]; termSetId: Guid; pageSize: number; selectedPanelOptions: ITag[]; @@ -17,6 +17,7 @@ export interface ITaxonomyFormProps { onResolveSuggestions: (filter: string, selectedItems?: ITag[]) => ITag[] | PromiseLike; onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; getTermSetInfo: (termSetId: Guid) => Promise; + placeHolder: string; } export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { @@ -27,16 +28,16 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { - setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, props.termSetId.toString()]); - props.getTermSetInfo(props.termSetId) .then((termSetInfo) => { const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; const termSetName = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag)[0].name; - const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: false }; + const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: termSetInfo.childrenCount > 0 }; setGroups([rootGroup]); - props.onLoadMoreData(props.termSetId, Guid.empty, '', true) + setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, termSetInfo.id]); + if (termSetInfo.childrenCount > 0) { + props.onLoadMoreData(props.termSetId, Guid.empty, '', true) .then((terms) => { const grps: IGroup[] = terms.value.map(term => { const g: IGroup = { @@ -60,7 +61,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement prevGroupsLoading.filter((value) => value !== props.termSetId.toString())); setGroups([rootGroup]); }); - + } }); }, []); @@ -154,8 +155,9 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { if (groupHeaderProps.group.level === 0) { + const labelStyles: IStyleFunctionOrObject = {root: {fontWeight: "normal"}}; return ( - + ); } if (props.allowMultipleSelections) { @@ -179,21 +181,18 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement = isSelected ? { choiceFieldWrapper: { fontWeight: 'bold' } } : { choiceFieldWrapper: { fontWeight: 'normal' } }; - const getClassNames = classNamesFunction(); - - const classNames = getClassNames(selectedStyle!, { - theme: undefined, - hasIcon: false, - hasImage: false, - checked: false, - disabled: false, - imageIsLarge: false, - imageSize: undefined, - focused: false, - }); + const selectedStyle: IStyleFunctionOrObject = isSelected ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } }; const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false; - const options: IChoiceGroupOption[] = [{ key: groupHeaderProps.group.key, text: groupHeaderProps.group.name, styles: selectedStyle, onRenderLabel: (p) => }]; + const options: IChoiceGroupOption[] = [{ + key: groupHeaderProps.group.key, + text: groupHeaderProps.group.name, + styles: selectedStyle, + onRenderLabel: (p) => + + {p.text} + + }]; + return ( { - const headerCountStyle = { display: 'none' }; - const checkButtonStyle = { display: 'none' }; - const expandStyle = { visibility: 'hidden' }; const groupHeaderStyles: IStyleFunctionOrObject = { - expand: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null, - expandIsCollapsed: !headerProps.group.children || headerProps.group.level === 0 ? expandStyle : null, - check: checkButtonStyle, - headerCount: headerCountStyle, + expand: { height: 42, visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" }, + expandIsCollapsed: { visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" }, + check: { display: 'none' }, + headerCount: { display: 'none' }, groupHeaderContainer: { height: 36, paddingTop: 3, paddingBottom: 3, paddingLeft: 3, paddingRight: 3, alignItems: 'center', }, - root: { height: 42 } + root: { height: 42 }, }; return ( @@ -243,6 +239,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { + setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, footerProps.group.key]); props.onLoadMoreData(props.termSetId, footerProps.group.key === props.termSetId.toString() ? Guid.empty : Guid.parse(footerProps.group.key), footerProps.group.data.skiptoken, true) .then((terms) => { const grps: IGroup[] = terms.value.map(term => { @@ -268,7 +265,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement - Load more... + {strings.ModernTaxonomyPickerLoadMoreText}
); @@ -295,14 +292,14 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement = { text: { borderStyle: 'none', borderWidth: '0px' } }; + const tagPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4}, input: {height: 34}, text: { borderStyle: 'none', borderWidth: '0px' } }; return (
- +
{ ListItemCommentsDialogDeleteTitle: "Confirm Delete Comment", ListItemCommentsLabel: "Comments", ListItemCommentsNoCommentsLabel: "There is no Comments", - OrgAssetsLinkLabel: "Your organisation" + OrgAssetsLinkLabel: "Your organisation", + + ModernTaxonomyPickerDefaultPlaceHolder: "Type term to tag", + ModernTaxonomyPickerTreeTitle: "Select one or more tags", + ModernTaxonomyPickerAddTagButtonTooltip: "Add Tag", + ModernTaxonomyPickerApplyButtonText: "Apply", + ModernTaxonomyPickerCancelButtonText: "Cancel", + ModernTaxonomyPickerLoadMoreText: "Load more", + ModernTaxonomyPickerRemoveButtonText: "Remove" }; }); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 90271ff6f..60a303053 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -363,6 +363,15 @@ declare interface IControlStrings { // Location picker customDisplayName:string; + + // Modern taxonomy picker + ModernTaxonomyPickerDefaultPlaceHolder: string; + ModernTaxonomyPickerTreeTitle: string; + ModernTaxonomyPickerAddTagButtonTooltip: string; + ModernTaxonomyPickerApplyButtonText: string; + ModernTaxonomyPickerCancelButtonText: string; + ModernTaxonomyPickerLoadMoreText: string; + ModernTaxonomyPickerRemoveButtonText: string; } declare interface IDateTimeStrings { diff --git a/src/loc/sv-se.ts b/src/loc/sv-se.ts index 71b445a5e..b0f2222b6 100644 --- a/src/loc/sv-se.ts +++ b/src/loc/sv-se.ts @@ -368,6 +368,13 @@ define([], () => { "ListItemCommentsLabel": "Kommentarer", "ListItemCommentsNoCommentsLabel": "Det finns inga kommentarer", "OrgAssetsLinkLabel": "Din organisation", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Ange en term som du vill tagga", + "ModernTaxonomyPickerTreeTitle": "Välj en tagg", + "ModernTaxonomyPickerAddTagButtonTooltip": "Lägg till tagg", + "ModernTaxonomyPickerApplyButtonText": "Använd", + "ModernTaxonomyPickerCancelButtonText": "Avbryt", + "ModernTaxonomyPickerLoadMoreText": "Läs in mer", + "ModernTaxonomyPickerRemoveButtonText": "Ta bort" }; -}); \ No newline at end of file +}); From a94a790ecf11e525fc467eaf6cea8ab488034bd0 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 8 Jun 2021 18:18:27 +0200 Subject: [PATCH 03/19] Fix for label localization and some code cleanup --- .../ModernTaxonomyPicker.module.scss | 168 ++---------------- .../ModernTaxonomyPicker.tsx | 53 +++--- .../taxonomyForm/TaxonomyForm.module.scss | 65 ------- .../taxonomyForm/TaxonomyForm.tsx | 10 +- 4 files changed, 41 insertions(+), 255 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss index d4830beae..82206dcff 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.module.scss @@ -1,167 +1,25 @@ .modernTaxonomyPicker { -.contextualMenu { - display: inline-block -} - -.listItem { - min-height: 36px; - line-height: 36px; - cursor: pointer; - - >div { - display: inline-block; - margin-right: 10px; - } - - img { - margin-right: 5px; - vertical-align: middle; - } -} - -.termField { - align-items: center; - border-spacing: 0; - display: flex; - width: 100%; - - .termFieldInput { - flex-grow: 1; - } - - .termFieldButton { - text-align: center; - width: 42px; - } - - input[type="text"] { - cursor: pointer; - opacity: 0.8; - width: 100%; - } -} - -.termset { - cursor: pointer; - 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; - - .termEnabled, - .termDisabled, - .termNoTagging { - background-repeat: no-repeat; - background-position: 30px center; - } - - .termEnabled { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACzSURBVDhPY2AYNKCoqIgTiOcD8X8S8F6wB4Aa1IH4akNDw+mPHz++/E8EuHTp0jmQRSDNCcXFxa/XrVt3gAh9KEpgBvx/9OjRLVI1g9TDDYBp3rlz5//Kysr/IJoYgGEASPPatWsbQDQxAMOAbdu2gZ0FookBcAOePHlyhxgN6GqQY+Hdhg0bDpJqCNgAaDrQAnJuNDY2nvr06dMbYgw6e/bsabgBUEN4yEiJ2wdNViLfIQC3sTh2vtJcswAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTerm.png - } - - .termDisabled { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFMSURBVDhPzZPNSsNAFIULQqEQEPoSQqAg7gqC0HdwXxAElyYgBBGSZ+gDdOUyIauULATdKrgIKCKuQjbiDySCkNV4TrkzpJW2cefAxwwzc86de2em0/k3zXGcHpgC9Qeu5glAsAMefN+/K8vyVbVoWZbdMxDFY9d136Ioum6hW9iiDVSe588rxDXmJ+AAdAWOOVcbAy1O01R5nqfYoxVglyk+Hu7Z4FiwOcc1GBRMwQSnOAxDHz0jDyCwwCVQS3DO0gU0BkmSzG8A/UQiz7DxC5yLGQ1PwDeYGYOiKF6WarCPDUOJeor+A4z0m8P4SNaG+hY+4zi+aZh0scEBNeB41DTBuCcGjj6FjaM/BUFwW1XVO6vdMNiSdIzJLwN5TJZ+iSLQKYwbR9cmZyaFdX+JhZIiMue+cLFQxA0G22uusd/6I8OEb4LXRwZN4Q+3Ys8Mb9+nRgAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTermDeprecated.png - } - - .termNoTagging { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZBJREFUeNqkU0trwkAQnhURBBFPKghekuJN7KW9pOCj0Agl9/4uT/6BHgXpxQfxYKltKEYhV3tXFF/4RrOdXYgkxpz8YA47O983O9+whFIKt8DvddHv9wej0UjY7/dwPB55sGaJRKImSVLxXMiSl9Hr9fRWq0WvoVKpUFVVv6xaF1nX9R+LvFqteKAgrVarVNM0fi6Xy9Sq99mfjeTv2Wz2mMvlYL1en/OGYYCiKKTT6fAzG8flQbfb1ReLxX0+n4fJZOLwQxRFKJVKNJPJwHa7dZuI5I/lcsnJrHMwGHQUpdNpHp5bGI/Hr7IsA3Pc7/dcDEynU5jP5xAIBJwCpmmeN+IF9IaT0VA20p+V5yaGQqF2s9mE3W4Hh8PBFXZyLBbTstmsaAkQqyuu7nOz2UiCIEAkEoFwOMzz6A0n44YgHo8bhULBYQaxP7vRaLyjyFsqleIiDLbOAyTfXY5GLueu1+s1XNULE2Fg5Gg0qj4jrnlDrhmHIu3hcPh0Op0gmUz+IvfBy1xy62/0wY34F2AAKtctO7g/KgIAAAAASUVORK5CYII='); // /_layouts/15/Images/EMMTermDisabled.png - } - - label>span { - padding-left: 25px; - } -} - -.actions { - button:first-child { - margin-right: 15px; - } -} - -.termBasePicker -{ - background-color: #fff; -} - .termSuggestion - { - min-height: 40px; + .termField { + align-items: center; + border-spacing: 0; + display: flex; width: 100%; - text-align: left; - cursor: pointer; - - .termSuggestionSubTitle - { - font-size: 12px; - color: #666666; + .termFieldInput { + flex-grow: 1; } - } - - .pickedTermRoot - { - position: relative; - outline: transparent; - box-sizing: content-box; - flex-shrink: 1; - background: #f4f4f4; - margin: 2px; - height: 26px; - line-height: 26px; - cursor: default; - display: flex; - flex-wrap: nowrap; - max-width: 300px; - - .pickedTermText - { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 30px; - margin: 0 8px; + .termFieldButton { + text-align: center; + width: 42px; } - .pickedTermCloseIcon - { + + input[type="text"] { cursor: pointer; - color: #666666; - font-size: 12px; - display: inline-block; - text-align: center; - vertical-align: top; - width: 30px; - height: 100%; - -ms-flex-negative: 0; - flex-shrink: 0; + opacity: 0.8; + width: 100%; } } - .errorMessage { - font-size: 12px; - font-weight: 400; - color: #a80000; - margin: 0; - padding-top: 5px; - display: flex; - align-items: center; - } - - .errorIcon { - font-size: 14px; - margin-right: 5px; - } - } diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 05876812d..0fb539299 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -26,7 +26,6 @@ export interface IModernTaxonomyPickerProps { label: string; context: BaseComponentContext; initialValues?: ITag[]; - errorMessage?: string; // TODO: is this needed? disabled?: boolean; required?: boolean; onChange?: (newValue?: ITag[]) => void; @@ -35,30 +34,22 @@ export interface IModernTaxonomyPickerProps { export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const [termsService] = React.useState(() => new SPTaxonomyService(props.context)); - const [errorMessage, setErrorMessage] = React.useState(props.errorMessage); - const [internalErrorMessage, setInternalErrorMessage] = React.useState(); const [panelIsOpen, setPanelIsOpen] = React.useState(false); - const [selectedOptions, setSelectedOptions] = React.useState([]); + const [selectedOptions, setSelectedOptions] = React.useState(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []); const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); - const invalidTerm = React.useRef(null); - React.useEffect(() => { sp.setup(props.context); }, []); - React.useEffect(() => { - if(Object.prototype.toString.call(props.initialValues) === '[object Array]' ) { - setSelectedOptions(props.initialValues); - } - else { - setSelectedOptions([]); - } - }, [props.initialValues]); - - React.useEffect(() => { - setErrorMessage(props.errorMessage); - }, [props.errorMessage]); + // React.useEffect(() => { + // if(Object.prototype.toString.call(props.initialValues) === '[object Array]' ) { + // setSelectedOptions(props.initialValues); + // } + // else { + // setSelectedOptions([]); + // } + // }, []); React.useEffect(() => { if (props.onChange) { @@ -66,7 +57,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { } }, [selectedOptions]); - async function onOpenPanel(): Promise { + function onOpenPanel(): void { if (props.disabled === true) { return; } @@ -108,30 +99,30 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { return filteredTags; } - const { label, disabled, allowMultipleSelections, panelTitle, required, placeHolder } = props; const calloutProps = { gapSpace: 0 }; const tooltipId = useId('tooltip'); const hostStyles: Partial = { root: { display: 'inline-block' } }; + const addTermButtonStyles: IButtonStyles = {rootHovered: {backgroundColor: "inherit"}, rootPressed: {backgroundColor: "inherit"}}; return (
- {label && } + {props.label && }
{ setSelectedOptions(itms || []); setSelectedPanelOptions(itms || []); }} - getTextFromItem={(tag: ITag, currentValue?: string) => tag.name} + getTextFromItem={(tag: ITag) => tag.name} inputProps={{ - 'aria-label': placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder, - placeholder: placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder + 'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder, + placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder }} />
@@ -142,20 +133,18 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { calloutProps={calloutProps} styles={hostStyles} > - +
- - { const horizontalGapStackTokens: IStackTokens = { childrenGap: 10, @@ -172,7 +161,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { props.termSetId && (
) diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss index 6801a69a4..87ac43d90 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.module.scss @@ -7,71 +7,6 @@ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } - .row { - @include ms-Grid-row; - // @include ms-fontColor-white; - // background-color: $ms-color-themeDark; - padding: 20px; - } - - .column { - @include ms-Grid-col; - @include ms-lg10; - @include ms-xl8; - @include ms-xlPush2; - @include ms-lgPush1; - } - - .title { - @include ms-font-xl; - // @include ms-fontColor-white; - } - - .subTitle { - @include ms-font-l; - // @include ms-fontColor-white; - } - - .description { - @include ms-font-l; - // @include ms-fontColor-white; - } - - .button { - // Our button - text-decoration: none; - height: 32px; - - // Primary Button - min-width: 80px; - background-color: $ms-color-themePrimary; - border-color: $ms-color-themePrimary; - color: $ms-color-white; - - // Basic Button - outline: transparent; - position: relative; - font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif; - -webkit-font-smoothing: antialiased; - font-size: $ms-font-size-m; - font-weight: $ms-font-weight-regular; - border-width: 0; - text-align: center; - cursor: pointer; - display: inline-block; - padding: 0 16px; - - .label { - font-weight: $ms-font-weight-semibold; - font-size: $ms-font-size-m; - height: 32px; - line-height: 32px; - margin: 0 4px; - vertical-align: top; - display: inline-block; - } - } - .choiceOption { color: "[theme: bodyText, default: #323130]"; padding-left: 26px; diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx index a820f3d3b..08cdf88e6 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx @@ -41,7 +41,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { const g: IGroup = { - name: term.labels?.[0].name, // TODO: fix this by looking up correct language + name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, key: term.id, startIndex: -1, count: 50, @@ -66,6 +66,8 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + if (group.isCollapsed === true) { setGroups((prevGroups) => { const recurseGroups = (currentGroup) => { @@ -95,7 +97,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { const g: IGroup = { - name: term.labels?.[0].name, // TODO: fix this by looking up correct language + name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, key: term.id, startIndex: -1, count: 50, @@ -227,6 +229,8 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { if ((footerProps.group.hasMoreData || footerProps.group.children && footerProps.group.children.length === 0) && !footerProps.group.isCollapsed) { + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + if (groupsLoading.some(value => value === footerProps.group.key)) { const spinnerStyles: IStyleFunctionOrObject = { circle: { verticalAlign: 'middle' } }; return ( @@ -244,7 +248,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { const g: IGroup = { - name: term.labels?.[0].name, // TODO: fix this by looking up correct language + name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, key: term.id, startIndex: -1, count: 50, From b2328973fbc79e30b1cf4b2bdd41b501a2f89892 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 8 Jun 2021 18:25:37 +0200 Subject: [PATCH 04/19] Added panel close button text --- src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx | 1 + src/loc/en-us.ts | 3 ++- src/loc/mystrings.d.ts | 1 + src/loc/sv-se.ts | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 0fb539299..c7fc2be47 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -141,6 +141,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { { ModernTaxonomyPickerApplyButtonText: "Apply", ModernTaxonomyPickerCancelButtonText: "Cancel", ModernTaxonomyPickerLoadMoreText: "Load more", - ModernTaxonomyPickerRemoveButtonText: "Remove" + ModernTaxonomyPickerRemoveButtonText: "Remove", + ModernTaxonomyPickerPanelCloseButtonText: "Close" }; }); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 60a303053..5cd933c1a 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -372,6 +372,7 @@ declare interface IControlStrings { ModernTaxonomyPickerCancelButtonText: string; ModernTaxonomyPickerLoadMoreText: string; ModernTaxonomyPickerRemoveButtonText: string; + ModernTaxonomyPickerPanelCloseButtonText: string; } declare interface IDateTimeStrings { diff --git a/src/loc/sv-se.ts b/src/loc/sv-se.ts index b0f2222b6..2a2cb4ae4 100644 --- a/src/loc/sv-se.ts +++ b/src/loc/sv-se.ts @@ -375,6 +375,7 @@ define([], () => { "ModernTaxonomyPickerApplyButtonText": "Använd", "ModernTaxonomyPickerCancelButtonText": "Avbryt", "ModernTaxonomyPickerLoadMoreText": "Läs in mer", - "ModernTaxonomyPickerRemoveButtonText": "Ta bort" + "ModernTaxonomyPickerRemoveButtonText": "Ta bort", + "ModernTaxonomyPickerPanelCloseButtonText": "Stäng" }; }); From 9eafd589860f690385a5bf9be42a151c5a4a3aa4 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 8 Jun 2021 21:18:34 +0200 Subject: [PATCH 05/19] Code cleanup --- .../modernTaxonomyPicker/ModernTaxonomyPicker.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index c7fc2be47..1c133ec60 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -6,12 +6,9 @@ import { PrimaryButton, DefaultButton, IconButton, IButtonStyles } from 'office- import { Label } from 'office-ui-fabric-react/lib/Label'; import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel'; import { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers'; -import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import { IStackTokens, Stack } from 'office-ui-fabric-react/lib/Stack'; import { sp } from '@pnp/sp'; -import { ITermInfo } from '@pnp/sp/taxonomy'; import { SPTaxonomyService } from '../../services/SPTaxonomyService'; -import FieldErrorMessage from '../errorMessage/ErrorMessage'; import { TaxonomyForm } from './taxonomyForm'; import styles from './ModernTaxonomyPicker.module.scss'; import * as strings from 'ControlStrings'; @@ -42,15 +39,6 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { sp.setup(props.context); }, []); - // React.useEffect(() => { - // if(Object.prototype.toString.call(props.initialValues) === '[object Array]' ) { - // setSelectedOptions(props.initialValues); - // } - // else { - // setSelectedOptions([]); - // } - // }, []); - React.useEffect(() => { if (props.onChange) { props.onChange(selectedOptions); From fec37998c9d3904c87a6a9bac1b548886ca8fba2 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 8 Jun 2021 21:21:10 +0200 Subject: [PATCH 06/19] Code cleanup --- src/services/SPTaxonomyService.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/services/SPTaxonomyService.ts b/src/services/SPTaxonomyService.ts index 168266da1..b3be0b1f9 100644 --- a/src/services/SPTaxonomyService.ts +++ b/src/services/SPTaxonomyService.ts @@ -5,14 +5,6 @@ import { SharePointQueryableCollection, sp } from '@pnp/sp'; import '@pnp/sp/taxonomy'; import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy'; -export interface ODataCollection { - '@odata.context': string; - '@odata.nextLink'?:string; - value: T[]; -} - -const EmptyODataCollection = { '@odata.context': '', value: [] }; - export class SPTaxonomyService { /** From 426d1e535b84cd18d0c6bff035dec7aaa6574774 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Wed, 9 Jun 2021 18:01:23 +0200 Subject: [PATCH 07/19] Check labels against user locale or term store locale instead of web locale --- .../ModernTaxonomyPicker.tsx | 24 +++++++++---- .../taxonomyForm/TaxonomyForm.tsx | 34 ++++++++++++++----- src/services/SPTaxonomyService.ts | 7 +++- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 1c133ec60..293ba40fc 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -15,6 +15,7 @@ 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 { ITermStoreInfo } from '@pnp/sp/taxonomy'; export interface IModernTaxonomyPickerProps { allowMultipleSelections: boolean; termSetId: string; @@ -30,13 +31,18 @@ export interface IModernTaxonomyPickerProps { } export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { - const [termsService] = React.useState(() => new SPTaxonomyService(props.context)); + const [taxonomyService] = React.useState(() => new SPTaxonomyService(props.context)); const [panelIsOpen, setPanelIsOpen] = React.useState(false); const [selectedOptions, setSelectedOptions] = React.useState(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []); const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); + const [termStoreInfo, setTermStoreInfo] = React.useState(); React.useEffect(() => { sp.setup(props.context); + taxonomyService.getTermStoreInfo() + .then((termStoreInfo) => { + setTermStoreInfo(termStoreInfo); + }); }, []); React.useEffect(() => { @@ -64,11 +70,11 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { } async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : termStoreInfo.defaultLanguageTag; if (filter === '') { return []; } - const filteredTerms = await termsService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : undefined); + const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : undefined); const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => { if (!selectedItems || selectedItems.length === 0) { return true; @@ -80,8 +86,11 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable); const filteredTags = filteredTermsAndAvailable.map((term) => { const key = term.id; - const name = term.labels.filter((termLabel) => (languageTag === '' || termLabel.languageTag === languageTag) && - termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name; + let labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag)); + if (labelsWithMatchingLanguageTag.length === 0) { + labelsWithMatchingLanguageTag = term.labels.filter((termLabel) => (termLabel.languageTag === termStoreInfo.defaultLanguageTag)); + } + const name = labelsWithMatchingLanguageTag.filter((termLabel) => termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name; return { key: key, name: name }; }); return filteredTags; @@ -152,8 +161,9 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { ITag[] | PromiseLike; onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; getTermSetInfo: (termSetId: Guid) => Promise; + termStoreInfo: ITermStoreInfo; placeHolder: string; } @@ -30,9 +31,12 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { props.getTermSetInfo(props.termSetId) .then((termSetInfo) => { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; - - const termSetName = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag)[0].name; + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; + let termSetNames = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag); + if (termSetNames.length === 0) { + termSetNames = termSetInfo.localizedNames.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag); + } + const termSetName = termSetNames[0].name; const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: termSetInfo.childrenCount > 0 }; setGroups([rootGroup]); setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, termSetInfo.id]); @@ -40,8 +44,12 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + if (termNames.length === 0) { + termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); + } const g: IGroup = { - name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, + name: termNames[0]?.name, key: term.id, startIndex: -1, count: 50, @@ -66,7 +74,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; if (group.isCollapsed === true) { setGroups((prevGroups) => { @@ -96,8 +104,12 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + if (termNames.length === 0) { + termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); + } const g: IGroup = { - name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, + name: termNames[0]?.name, key: term.id, startIndex: -1, count: 50, @@ -229,7 +241,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { if ((footerProps.group.hasMoreData || footerProps.group.children && footerProps.group.children.length === 0) && !footerProps.group.isCollapsed) { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName; + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; if (groupsLoading.some(value => value === footerProps.group.key)) { const spinnerStyles: IStyleFunctionOrObject = { circle: { verticalAlign: 'middle' } }; @@ -247,8 +259,12 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { const grps: IGroup[] = terms.value.map(term => { + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + if (termNames.length === 0) { + termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); + } const g: IGroup = { - name: term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true))[0]?.name, + name: termNames[0]?.name, key: term.id, startIndex: -1, count: 50, diff --git a/src/services/SPTaxonomyService.ts b/src/services/SPTaxonomyService.ts index b3be0b1f9..1be1ff9bd 100644 --- a/src/services/SPTaxonomyService.ts +++ b/src/services/SPTaxonomyService.ts @@ -3,7 +3,7 @@ import { Guid } from '@microsoft/sp-core-library'; import { LambdaParser } from '@pnp/odata/parsers'; import { SharePointQueryableCollection, sp } from '@pnp/sp'; import '@pnp/sp/taxonomy'; -import { ITermInfo, ITermSetInfo } from '@pnp/sp/taxonomy'; +import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; export class SPTaxonomyService { @@ -76,4 +76,9 @@ export class SPTaxonomyService { const tsInfo = await sp.termStore.sets.getById(termSetId.toString()).get(); return tsInfo; } + + public async getTermStoreInfo(): Promise { + const termStoreInfo = await sp.termStore(); + return termStoreInfo; + } } From e9f353fcab24445aba96219f7e13eb28d41d4605 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Wed, 9 Jun 2021 18:05:28 +0200 Subject: [PATCH 08/19] Changed variable name --- src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 293ba40fc..488b23bb7 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -40,8 +40,8 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { React.useEffect(() => { sp.setup(props.context); taxonomyService.getTermStoreInfo() - .then((termStoreInfo) => { - setTermStoreInfo(termStoreInfo); + .then((localTermStoreInfo) => { + setTermStoreInfo(localTermStoreInfo); }); }, []); From f5f70c5fd04e4ff21d0a2cb29ff99c2b2bfe2cac Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Thu, 10 Jun 2021 17:16:29 +0200 Subject: [PATCH 09/19] Styling, anchor term handling, new selection handling --- .../ModernTaxonomyPicker.tsx | 29 ++- .../taxonomyForm/TaxonomyForm.tsx | 192 ++++++++++++------ src/services/SPTaxonomyService.ts | 22 +- 3 files changed, 159 insertions(+), 84 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 488b23bb7..2a063aa4c 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -5,8 +5,9 @@ import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon'; 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 { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers'; +import { IBasePickerStyleProps, IBasePickerStyles, ITag, TagPicker } 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'; import { TaxonomyForm } from './taxonomyForm'; @@ -15,7 +16,8 @@ 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 { ITermStoreInfo } from '@pnp/sp/taxonomy'; +import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; + export interface IModernTaxonomyPickerProps { allowMultipleSelections: boolean; termSetId: string; @@ -36,6 +38,8 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const [selectedOptions, setSelectedOptions] = React.useState(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []); const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); const [termStoreInfo, setTermStoreInfo] = React.useState(); + const [termSetInfo, setTermSetInfo] = React.useState(); + const [anchorTermInfo, setAnchorTermInfo] = React.useState(); React.useEffect(() => { sp.setup(props.context); @@ -43,6 +47,16 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { .then((localTermStoreInfo) => { setTermStoreInfo(localTermStoreInfo); }); + taxonomyService.getTermSetInfo(Guid.parse(props.termSetId)) + .then((localTermSetInfo) => { + setTermSetInfo(localTermSetInfo); + }); + 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); + }); + } }, []); React.useEffect(() => { @@ -74,14 +88,12 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { if (filter === '') { return []; } - const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : undefined); + const filteredTerms = await taxonomyService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : Guid.empty); const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => { if (!selectedItems || selectedItems.length === 0) { return true; } - for (const selectedItem of selectedItems) { - return selectedItem.key !== term.id; - } + return selectedItems.every((item) => item.key !== term.id); }); const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable); const filteredTags = filteredTermsAndAvailable.map((term) => { @@ -100,6 +112,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const tooltipId = useId('tooltip'); const hostStyles: Partial = { root: { display: 'inline-block' } }; const addTermButtonStyles: IButtonStyles = {rootHovered: {backgroundColor: "inherit"}, rootPressed: {backgroundColor: "inherit"}}; + const tagPickerStyles: IStyleFunctionOrObject = { input: {minheight: 34}, text: {minheight: 34} }; return (
@@ -112,6 +125,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { itemLimit={props.allowMultipleSelections ? undefined : 1} selectedItems={selectedOptions} disabled={props.disabled} + styles={tagPickerStyles} onChange={(itms?: ITag[]) => { setSelectedOptions(itms || []); setSelectedPanelOptions(itms || []); @@ -162,7 +176,8 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { allowMultipleSelections={props.allowMultipleSelections} onResolveSuggestions={onResolveSuggestions} onLoadMoreData={taxonomyService.getTerms} - getTermSetInfo={taxonomyService.getTermSetInfo} + anchorTermInfo={anchorTermInfo} + termSetInfo={termSetInfo} termStoreInfo={termStoreInfo} context={props.context} termSetId={Guid.parse(props.termSetId)} diff --git a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx index 15ab3158f..6411c4e05 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyForm/TaxonomyForm.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import styles from './TaxonomyForm.module.scss'; -import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupedList, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Spinner, TagPicker } from 'office-ui-fabric-react'; +import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Selection, Spinner, TagPicker } from 'office-ui-fabric-react'; import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; import { Guid } from '@microsoft/sp-core-library'; import { BaseComponentContext } from '@microsoft/sp-component-base'; import { css } from '@uifabric/utilities/lib/css'; import * as strings from 'ControlStrings'; +import { useForceUpdate } from '@uifabric/react-hooks'; export interface ITaxonomyFormProps { context: BaseComponentContext; @@ -16,61 +17,96 @@ export interface ITaxonomyFormProps { setSelectedPanelOptions: React.Dispatch>; onResolveSuggestions: (filter: string, selectedItems?: ITag[]) => ITag[] | PromiseLike; onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; - getTermSetInfo: (termSetId: Guid) => Promise; + anchorTermInfo: ITermInfo; + termSetInfo: ITermSetInfo; termStoreInfo: ITermStoreInfo; placeHolder: string; } export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { - const groupedListRef = React.useRef(); - const [groupsLoading, setGroupsLoading] = React.useState([]); const [groups, setGroups] = React.useState([]); + const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); + + const forceUpdate = useForceUpdate(); + const selection = React.useMemo(() => { + const s = new Selection({onSelectionChanged: () => { + props.setSelectedPanelOptions((prevOptions) => [...selection.getSelection()]); + forceUpdate(); + }}); + s.setItems(terms); + for (const selectedOption of props.selectedPanelOptions) { + if (s.canSelectItem) { + s.setKeySelected(selectedOption.key.toString(), true, true); + } + } + return s; + }, [terms]); React.useEffect(() => { - props.getTermSetInfo(props.termSetId) - .then((termSetInfo) => { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; - let termSetNames = termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag); - if (termSetNames.length === 0) { - termSetNames = termSetInfo.localizedNames.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag); - } - const termSetName = termSetNames[0].name; - const rootGroup: IGroup = { name: termSetName, key: termSetInfo.id, startIndex: -1, count: 50, level: 0, isCollapsed: false, data: { skiptoken: '' }, hasMoreData: termSetInfo.childrenCount > 0 }; + const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; + let termRootName = ""; + if (props.anchorTermInfo) { + let anchorTermNames = props.anchorTermInfo.labels.filter((name) => name.languageTag === languageTag && name.isDefault); + if (anchorTermNames.length === 0) { + anchorTermNames = props.anchorTermInfo.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); + } + termRootName = anchorTermNames[0].name; + } + else { + let termSetNames = props.termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag); + if (termSetNames.length === 0) { + termSetNames = props.termSetInfo.localizedNames.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag); + } + termRootName = termSetNames[0].name; + } + const rootGroup: IGroup = { + name: termRootName, + key: props.anchorTermInfo ? props.anchorTermInfo.id : props.termSetInfo.id, + startIndex: -1, + count: 50, + level: 0, + isCollapsed: false, + data: { skiptoken: '' }, + hasMoreData: (props.anchorTermInfo ? props.anchorTermInfo.childrenCount : props.termSetInfo.childrenCount) > 0 + }; + setGroups([rootGroup]); + setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, props.termSetInfo.id]); + if (props.termSetInfo.childrenCount > 0) { + props.onLoadMoreData(props.termSetId, props.anchorTermInfo ? Guid.parse(props.anchorTermInfo.id) : Guid.empty, '', true) + .then((loadedTerms) => { + const grps: IGroup[] = loadedTerms.value.map(term => { + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + if (termNames.length === 0) { + termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); + } + const g: IGroup = { + name: termNames[0]?.name, + key: term.id, + startIndex: -1, + count: 50, + level: 1, + isCollapsed: true, + data: { skiptoken: '', term: term }, + hasMoreData: term.childrenCount > 0, + }; + if (g.hasMoreData) { + g.children = []; + } + return g; + }); + setTerms((prevTerms) => { + const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); + return [...prevTerms, ...nonExistingTerms]; + }); + rootGroup.children = grps; + rootGroup.data.skiptoken = loadedTerms.skiptoken; + rootGroup.hasMoreData = loadedTerms.skiptoken !== ''; + setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== props.termSetId.toString())); setGroups([rootGroup]); - setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, termSetInfo.id]); - if (termSetInfo.childrenCount > 0) { - props.onLoadMoreData(props.termSetId, Guid.empty, '', true) - .then((terms) => { - const grps: IGroup[] = terms.value.map(term => { - let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); - if (termNames.length === 0) { - termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); - } - const g: IGroup = { - name: termNames[0]?.name, - key: term.id, - startIndex: -1, - count: 50, - level: 1, - isCollapsed: true, - data: { skiptoken: '', term: term }, - hasMoreData: term.childrenCount > 0, - }; - if (g.hasMoreData) { - g.children = []; - } - return g; - }); - rootGroup.children = grps; - rootGroup.data.skiptoken = terms.skiptoken; - rootGroup.hasMoreData = terms.skiptoken !== ''; - setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== props.termSetId.toString())); - setGroups([rootGroup]); - }); - } }); + } }, []); const onToggleCollapse = (group: IGroup): void => { @@ -102,8 +138,8 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { - const grps: IGroup[] = terms.value.map(term => { + .then((loadedTerms) => { + const grps: IGroup[] = loadedTerms.value.map(term => { let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); if (termNames.length === 0) { termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); @@ -123,9 +159,13 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { + const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); + return [...prevTerms, ...nonExistingTerms]; + }); group.children = grps; - group.data.skiptoken = terms.skiptoken; - group.hasMoreData = terms.skiptoken !== ''; + group.data.skiptoken = loadedTerms.skiptoken; + group.hasMoreData = loadedTerms.skiptoken !== ''; setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== group.key)); }); } @@ -155,28 +195,37 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement, option?: IChoiceGroupOption): void => { - props.setSelectedPanelOptions([{ key: option.key, name: option.text }]); + selection.setAllSelected(false); + selection.setKeySelected(option.key, true, true); }; const onCheckboxChange = (ev?: React.FormEvent, checked?: boolean, tag?: ITag): void => { if (checked) { - props.setSelectedPanelOptions((prevOptions) => [...prevOptions, tag]); + selection.setKeySelected(tag.key.toString(), true, true); } else { - props.setSelectedPanelOptions((prevOptions) => prevOptions.filter((value) => value.key !== tag.key)); + selection.setKeySelected(tag.key.toString(), false, true); } }; const onRenderTitle = (groupHeaderProps: IGroupHeaderProps) => { + const isChildSelected = (children: IGroup[]): boolean => { + let aChildIsSelected = children && children.some((child) => selection.isKeySelected(child.key) || isChildSelected(child.children)); + return aChildIsSelected; + }; + + const isBold = isChildSelected(groupHeaderProps.group.children); + if (groupHeaderProps.group.level === 0) { - const labelStyles: IStyleFunctionOrObject = {root: {fontWeight: "normal"}}; + const labelStyles: IStyleFunctionOrObject = {root: {fontWeight: isBold ? "bold" : "normal"}}; return ( ); } + if (props.allowMultipleSelections) { - const isSelected = props.selectedPanelOptions.some(value => value.key === groupHeaderProps.group.key); - const selectedStyles: IStyleFunctionOrObject = isSelected ? { label: { fontWeight: 'bold' } } : { label: { fontWeight: 'normal' } }; + const isSelected = selection.isKeySelected(groupHeaderProps.group.key); + const selectedStyles: IStyleFunctionOrObject = isSelected || isBold ? { label: { fontWeight: 'bold' } } : { label: { fontWeight: 'normal' } }; return ( = isSelected ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } }; + const isSelected = selection.isKeySelected(groupHeaderProps.group.key); + const selectedStyle: IStyleFunctionOrObject = isSelected || isBold ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } }; const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false; const options: IChoiceGroupOption[] = [{ key: groupHeaderProps.group.key, @@ -210,7 +259,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement @@ -257,8 +306,8 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, footerProps.group.key]); props.onLoadMoreData(props.termSetId, footerProps.group.key === props.termSetId.toString() ? Guid.empty : Guid.parse(footerProps.group.key), footerProps.group.data.skiptoken, true) - .then((terms) => { - const grps: IGroup[] = terms.value.map(term => { + .then((loadedTerms) => { + const grps: IGroup[] = loadedTerms.value.map(term => { let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); if (termNames.length === 0) { termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); @@ -278,9 +327,13 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { + const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); + return [...prevTerms, ...nonExistingTerms]; + }); footerProps.group.children = [...footerProps.group.children, ...grps]; - footerProps.group.data.skiptoken = terms.skiptoken; - footerProps.group.hasMoreData = terms.skiptoken !== ''; + footerProps.group.data.skiptoken = loadedTerms.skiptoken; + footerProps.group.hasMoreData = loadedTerms.skiptoken !== ''; setGroupsLoading((prevGroupsLoading) => prevGroupsLoading.filter((value) => value !== footerProps.group.key)); }); }} @@ -308,11 +361,19 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement { - props.setSelectedPanelOptions(itms || []); + const onPickerChange = (items?: ITag[]): void => { + selection.setAllSelected(false); + const itemsToAdd = items.filter((item) => terms.every((term) => term.key !== item.key)); + if (itemsToAdd.length > 0) { + selection.setItems([...terms, ...itemsToAdd]); + setTerms((prevTerms) => [...prevTerms, ...itemsToAdd]); + } + for (const item of items) { + selection.setKeySelected(item.key.toString(), true, true); + } }; - const tagPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4}, input: {height: 34}, text: { borderStyle: 'none', borderWidth: '0px' } }; + const tagPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4, minheight: 34}, input: {minheight: 34}, text: { minheight: 34, borderStyle: 'none', borderWidth: '0px' } }; return (
@@ -322,7 +383,7 @@ export function TaxonomyForm(props: ITaxonomyFormProps): React.ReactElement{strings.ModernTaxonomyPickerTreeTitle}
{ - const batch = sp.createBatch(); - const results = []; - termIds.forEach(termId => { - sp.termStore.sets.getById(termSetId.toString()).terms.getById(termId).inBatch(batch)() - .then(term => results.push(term)) - .catch(reason => console.warn(`Error retreiving Term ID ${termId}: ${reason}`)); - }); - await batch.execute(); - return results; + public async getTermById(termSetId: Guid, termId: Guid): Promise { + if (termId === Guid.empty) { + return undefined; + } + try { + const termInfo = await sp.termStore.sets.getById(termSetId.toString()).terms.getById(termId.toString())(); + return termInfo; + } catch (error) { + return undefined; + } } public async searchTerm(termSetId: Guid, label: string, languageTag: string, parentTermId?: Guid, stringMatchId: string = '0', pageSize: number = 50): Promise { try { - const searchTermUrl = sp.termStore.concat(`/searchTerm(label='${label}',setId='${termSetId}',languageTag='${languageTag}',stringMatchId='${stringMatchId}'${parentTermId ? `,parentTermId='${parentTermId}'` : ''})`).toUrl(); + const searchTermUrl = sp.termStore.concat(`/searchTerm(label='${label}',setId='${termSetId}',languageTag='${languageTag}',stringMatchId='${stringMatchId}'${parentTermId && parentTermId !== Guid.empty ? `,parentTermId='${parentTermId}'` : ''})`).toUrl(); const searchTermQuery = SharePointQueryableCollection(searchTermUrl).top(pageSize); const filteredTerms = await searchTermQuery(); return filteredTerms; From 1c00c21603cebd5c51c409a12e879d9cadcd97df Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Thu, 10 Jun 2021 17:24:32 +0200 Subject: [PATCH 10/19] Changed name to TaxonomyPanelContents --- src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx | 4 ++-- src/controls/modernTaxonomyPicker/taxonomyForm/index.ts | 1 - .../TaxonomyPanelContents.module.scss} | 0 .../TaxonomyPanelContents.tsx} | 4 ++-- .../modernTaxonomyPicker/taxonomyPanelContents/index.ts | 1 + 5 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 src/controls/modernTaxonomyPicker/taxonomyForm/index.ts rename src/controls/modernTaxonomyPicker/{taxonomyForm/TaxonomyForm.module.scss => taxonomyPanelContents/TaxonomyPanelContents.module.scss} (100%) rename src/controls/modernTaxonomyPicker/{taxonomyForm/TaxonomyForm.tsx => taxonomyPanelContents/TaxonomyPanelContents.tsx} (99%) create mode 100644 src/controls/modernTaxonomyPicker/taxonomyPanelContents/index.ts diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 2a063aa4c..d2ea1da14 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -10,7 +10,7 @@ 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'; -import { TaxonomyForm } from './taxonomyForm'; +import { TaxonomyPanelContents } from './taxonomyPanelContents'; import styles from './ModernTaxonomyPicker.module.scss'; import * as strings from 'ControlStrings'; import { TooltipHost } from '@microsoft/office-ui-fabric-react-bundle'; @@ -172,7 +172,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { { props.termSetId && (
- { +export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactElement { const [groupsLoading, setGroupsLoading] = React.useState([]); const [groups, setGroups] = React.useState([]); const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/index.ts b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/index.ts new file mode 100644 index 000000000..2c8bb7f58 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/index.ts @@ -0,0 +1 @@ +export * from './TaxonomyPanelContents'; From e0927010cb13d0d1536f8c947f3df2e74c7760f2 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Fri, 11 Jun 2021 00:08:00 +0200 Subject: [PATCH 11/19] Changed selection method and some styling --- .../TaxonomyPanelContents.module.scss | 8 +- .../TaxonomyPanelContents.tsx | 112 ++++++++++-------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss index 87ac43d90..5fcd78476 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss @@ -1,12 +1,6 @@ @import '~office-ui-fabric-react/dist/sass/References.scss'; -.taxonomyForm { - .container { - max-width: 700px; - margin: 0px auto; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); - } - +.taxonomyPanelContents { .choiceOption { color: "[theme: bodyText, default: #323130]"; padding-left: 26px; diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index b1f1d6d73..380065bb2 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import styles from './TaxonomyPanelContents.module.scss'; -import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Selection, Spinner, TagPicker } from 'office-ui-fabric-react'; +import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Selection, SelectionMode, SelectionZone, Spinner, TagPicker } from 'office-ui-fabric-react'; import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; import { Guid } from '@microsoft/sp-core-library'; import { BaseComponentContext } from '@microsoft/sp-component-base'; @@ -194,58 +194,61 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle } }; - const onChoiceChange = (ev?: React.FormEvent, option?: IChoiceGroupOption): void => { - selection.setAllSelected(false); - selection.setKeySelected(option.key, true, true); - }; - - const onCheckboxChange = (ev?: React.FormEvent, checked?: boolean, tag?: ITag): void => { - if (checked) { - selection.setKeySelected(tag.key.toString(), true, true); - } - else { - selection.setKeySelected(tag.key.toString(), false, true); - } - }; - const onRenderTitle = (groupHeaderProps: IGroupHeaderProps) => { const isChildSelected = (children: IGroup[]): boolean => { let aChildIsSelected = children && children.some((child) => selection.isKeySelected(child.key) || isChildSelected(child.children)); return aChildIsSelected; }; - const isBold = isChildSelected(groupHeaderProps.group.children); + const childIsSelected = isChildSelected(groupHeaderProps.group.children); if (groupHeaderProps.group.level === 0) { - const labelStyles: IStyleFunctionOrObject = {root: {fontWeight: isBold ? "bold" : "normal"}}; + const labelStyles: IStyleFunctionOrObject = {root: {fontWeight: childIsSelected ? "bold" : "normal"}}; return ( ); } + const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false; + const isSelected = selection.isKeySelected(groupHeaderProps.group.key); + + const selectionProps = { + "data-selection-index": selection.getItems().findIndex((term) => term.key === groupHeaderProps.group.key) + }; + if (props.allowMultipleSelections) { - const isSelected = selection.isKeySelected(groupHeaderProps.group.key); - const selectedStyles: IStyleFunctionOrObject = isSelected || isBold ? { label: { fontWeight: 'bold' } } : { label: { fontWeight: 'normal' } }; + if (isDisabled) { + selectionProps["data-selection-disabled"] = true; + } + else { + selectionProps["data-selection-toggle"] = true; + } + + const selectedStyles: IStyleFunctionOrObject = { root: {pointerEvents: 'none'} }; + if (isSelected || childIsSelected) { + selectedStyles.label = { fontWeight: 'bold' }; + } + else { + selectedStyles.label = { fontWeight: 'normal' }; + } return ( - , checked?: boolean) => - onCheckboxChange(ev, checked, { name: groupHeaderProps.group.name, key: groupHeaderProps.group.key })} - checked={isSelected} - styles={selectedStyles} - disabled={groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false} - onRenderLabel={(p) => - {p.label} - } - /> +
+ + {p.label} + } + /> +
); } else { - const isSelected = selection.isKeySelected(groupHeaderProps.group.key); - const selectedStyle: IStyleFunctionOrObject = isSelected || isBold ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } }; - const isDisabled = groupHeaderProps.group.data.term.isAvailableForTagging.filter((t) => t.setId === props.termSetId.toString())[0].isAvailable === false; + const selectedStyle: IStyleFunctionOrObject = isSelected || childIsSelected ? { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'bold', } } : { root: {marginTop: 0}, choiceFieldWrapper: { fontWeight: 'normal' } }; const options: IChoiceGroupOption[] = [{ key: groupHeaderProps.group.key, text: groupHeaderProps.group.name, @@ -256,13 +259,21 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle }]; + if (isDisabled) { + selectionProps["data-selection-disabled"] = true; + } + else { + selectionProps["data-selection-select"] = true; + } + return ( - +
+ +
); } }; @@ -376,7 +387,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle const tagPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4, minheight: 34}, input: {minheight: 34}, text: { minheight: 34, borderStyle: 'none', borderWidth: '0px' } }; return ( -
+
- ) => false} - /> + + ) => false} + /> +
); From d7d86a24a2c23f480f1c0c04f0ee8571f93404a2 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Mon, 20 Sep 2021 10:19:21 +0200 Subject: [PATCH 12/19] Changed from tag picker to term picker --- .../ModernTaxonomyPicker.tsx | 159 +++++++++++++----- src/controls/modernTaxonomyPicker/index.ts | 1 + .../modernTermPicker/ModernTermPicker.tsx | 52 ++++++ .../ModernTermPicker.types.ts | 60 +++++++ .../TaxonomyPanelContents.module.scss | 13 ++ .../TaxonomyPanelContents.tsx | 137 +++++++++------ .../termItem/TermItem.styles.ts | 134 +++++++++++++++ .../termItem/TermItem.tsx | 64 +++++++ .../termItem/TermItemSuggestion.styles.ts | 26 +++ .../termItem/TermItemSuggestion.tsx | 39 +++++ .../termItem/TermItemSuggestions.module.scss | 26 +++ src/services/SPTaxonomyService.ts | 5 +- 12 files changed, 626 insertions(+), 90 deletions(-) create mode 100644 src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx create mode 100644 src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts create mode 100644 src/controls/modernTaxonomyPicker/termItem/TermItem.styles.ts create mode 100644 src/controls/modernTaxonomyPicker/termItem/TermItem.tsx create mode 100644 src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.styles.ts create mode 100644 src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx create mode 100644 src/controls/modernTaxonomyPicker/termItem/TermItemSuggestions.module.scss diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index d2ea1da14..e477444b7 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -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'; @@ -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) => 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(Object.prototype.toString.call(props.initialValues) === '[object Array]' ? props.initialValues : []); - const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); - const [termStoreInfo, setTermStoreInfo] = React.useState(); - const [termSetInfo, setTermSetInfo] = React.useState(); - const [anchorTermInfo, setAnchorTermInfo] = React.useState(); + const [selectedOptions, setSelectedOptions] = React.useState([]); + const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); + const [currentTermStoreInfo, setCurrentTermStoreInfo] = React.useState(); + const [currentTermSetInfo, setCurrentTermSetInfo] = React.useState(); + const [currentAnchorTermInfo, setCurrentAnchorTermInfo] = React.useState(); + const [currentLanguageTag, setCurrentLanguageTag] = React.useState(""); 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]); @@ -83,29 +116,69 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { onClosePanel(); } - async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : termStoreInfo.defaultLanguageTag; + async function onResolveSuggestions(filter: string, selectedItems?: ITermInfoExt[]): Promise { 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 { + 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): JSX.Element { + return ( + + ); + } + + 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 ? ( + {labels[0].name} + ) : 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 }; @@ -119,22 +192,25 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { {props.label && }
- { + 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} />
@@ -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 = { @@ -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} />
) diff --git a/src/controls/modernTaxonomyPicker/index.ts b/src/controls/modernTaxonomyPicker/index.ts index bfa130c40..4d3768ab4 100644 --- a/src/controls/modernTaxonomyPicker/index.ts +++ b/src/controls/modernTaxonomyPicker/index.ts @@ -1 +1,2 @@ export * from './ModernTaxonomyPicker'; +export * from './termItem/TermItem'; diff --git a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx new file mode 100644 index 000000000..34cf8a0f9 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx @@ -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 { + 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 ? ( + {labels[0].name} + ) : null; + }, + onRenderSuggestionsItem: (props: ITermInfoExt, itemProps: ISuggestionItemProps) => { + const onLoadParentLabel = async (termId: Guid): Promise => { + return Promise.resolve(""); + }; + return ; + }, + }; + + constructor(props: IModernTermPickerProps) { + super(props); + initializeComponentRef(this); + } +} + +export const ModernTermPicker = styled( + ModernTermPickerBase, + getStyles, + undefined, + { + scope: 'ModernTermPicker', + }, +); diff --git a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts new file mode 100644 index 000000000..679b597fa --- /dev/null +++ b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts @@ -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 {} + +export interface ITermItemProps extends IPickerItemProps { + /** 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; + + /** Theme provided by High-Order Component. */ + theme?: ITheme; + termStoreInfo: ITermStoreInfo; + languageTag: string; +} + +export type ITermItemStyleProps = Required> & + Pick & {}; + +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 { + /** 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; + + /** Theme provided by High-Order Component. */ + theme?: ITheme; +} + +export type ITermItemSuggestionStyleProps = Required> & + Pick & {}; + +export interface ITermItemSuggestionStyles { + /** Refers to the text element of the TermItemSuggestion */ + suggestionTextOverflow?: IStyle; +} diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss index 5fcd78476..3be7fc38c 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss @@ -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; + } } diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index 380065bb2..2b9aa6a0d 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -1,61 +1,104 @@ import * as React from 'react'; import styles from './TaxonomyPanelContents.module.scss'; -import { Checkbox, ChoiceGroup, GroupedList, GroupHeader, IBasePickerStyleProps, IBasePickerStyles, ICheckboxStyleProps, ICheckboxStyles, IChoiceGroupOption, IChoiceGroupOptionStyleProps, IChoiceGroupOptionStyles, IGroup, IGroupFooterProps, IGroupHeaderProps, IGroupHeaderStyleProps, IGroupHeaderStyles, IGroupRenderProps, IGroupShowAllProps, ILabelStyleProps, ILabelStyles, ILinkStyleProps, ILinkStyles, IListProps, IRenderFunction, ISpinnerStyleProps, ISpinnerStyles, IStyleFunctionOrObject, ITag, Label, Link, Selection, SelectionMode, SelectionZone, Spinner, TagPicker } from 'office-ui-fabric-react'; -import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; +import { Checkbox, + ChoiceGroup, + GroupedList, + GroupHeader, + IBasePickerStyleProps, + IBasePickerStyles, + ICheckboxStyleProps, + ICheckboxStyles, + IChoiceGroupOption, + IChoiceGroupOptionStyleProps, + IChoiceGroupOptionStyles, + IGroup, + IGroupFooterProps, + IGroupHeaderProps, + IGroupHeaderStyleProps, + IGroupHeaderStyles, + IGroupRenderProps, + IGroupShowAllProps, + ILabelStyleProps, + ILabelStyles, + ILinkStyleProps, + ILinkStyles, + IListProps, + IPickerItemProps, + IRenderFunction, + ISpinnerStyleProps, + ISpinnerStyles, + IStyleFunctionOrObject, + ISuggestionItemProps, + Label, + Link, + Selection, + SelectionMode, + SelectionZone, + Spinner + } from 'office-ui-fabric-react'; +import { ITermInfo, + ITermSetInfo, + ITermStoreInfo + } from '@pnp/sp/taxonomy'; import { Guid } from '@microsoft/sp-core-library'; import { BaseComponentContext } from '@microsoft/sp-component-base'; import { css } from '@uifabric/utilities/lib/css'; import * as strings from 'ControlStrings'; import { useForceUpdate } from '@uifabric/react-hooks'; +import { ModernTermPicker } from '../modernTermPicker/ModernTermPicker'; +import { ITermInfoExt } from '../modernTermPicker/ModernTermPicker.types'; export interface ITaxonomyFormProps { context: BaseComponentContext; - allowMultipleSelections: boolean; + allowMultipleSelections?: boolean; termSetId: Guid; pageSize: number; - selectedPanelOptions: ITag[]; - setSelectedPanelOptions: React.Dispatch>; - onResolveSuggestions: (filter: string, selectedItems?: ITag[]) => ITag[] | PromiseLike; + selectedPanelOptions: ITermInfoExt[]; + setSelectedPanelOptions: React.Dispatch>; + onResolveSuggestions: (filter: string, selectedItems?: ITermInfoExt[]) => ITermInfoExt[] | PromiseLike; onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; anchorTermInfo: ITermInfo; termSetInfo: ITermSetInfo; termStoreInfo: ITermStoreInfo; placeHolder: string; + onRenderSuggestionsItem?: (props: ITermInfoExt, itemProps: ISuggestionItemProps) => JSX.Element; + onRenderItem?: (props: IPickerItemProps) => JSX.Element; + getTextFromItem: (item: ITermInfoExt, currentValue?: string) => string; + languageTag: string; } export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactElement { const [groupsLoading, setGroupsLoading] = React.useState([]); const [groups, setGroups] = React.useState([]); - const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); + const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); const forceUpdate = useForceUpdate(); const selection = React.useMemo(() => { - const s = new Selection({onSelectionChanged: () => { + const s = new Selection({onSelectionChanged: () => { props.setSelectedPanelOptions((prevOptions) => [...selection.getSelection()]); forceUpdate(); - }}); + }, getKey: (term: ITermInfoExt) => term.id}); s.setItems(terms); for (const selectedOption of props.selectedPanelOptions) { if (s.canSelectItem) { - s.setKeySelected(selectedOption.key.toString(), true, true); + s.setKeySelected(selectedOption.id.toString(), true, true); } } return s; }, [terms]); React.useEffect(() => { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; let termRootName = ""; if (props.anchorTermInfo) { - let anchorTermNames = props.anchorTermInfo.labels.filter((name) => name.languageTag === languageTag && name.isDefault); + let anchorTermNames = props.anchorTermInfo.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault); if (anchorTermNames.length === 0) { anchorTermNames = props.anchorTermInfo.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); } termRootName = anchorTermNames[0].name; } else { - let termSetNames = props.termSetInfo.localizedNames.filter((name) => name.languageTag === languageTag); + let termSetNames = props.termSetInfo.localizedNames.filter((name) => name.languageTag === props.languageTag); if (termSetNames.length === 0) { termSetNames = props.termSetInfo.localizedNames.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag); } @@ -77,7 +120,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle props.onLoadMoreData(props.termSetId, props.anchorTermInfo ? Guid.parse(props.anchorTermInfo.id) : Guid.empty, '', true) .then((loadedTerms) => { const grps: IGroup[] = loadedTerms.value.map(term => { - let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.languageTag && termLabel.isDefault === true)); if (termNames.length === 0) { termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); } @@ -97,8 +140,9 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle return g; }); setTerms((prevTerms) => { - const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); - return [...prevTerms, ...nonExistingTerms]; + const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); + const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); + return [...prevTerms, ...nonExistingTermsAsExt]; }); rootGroup.children = grps; rootGroup.data.skiptoken = loadedTerms.skiptoken; @@ -110,11 +154,9 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle }, []); const onToggleCollapse = (group: IGroup): void => { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; - if (group.isCollapsed === true) { setGroups((prevGroups) => { - const recurseGroups = (currentGroup) => { + const recurseGroups = (currentGroup: IGroup) => { if (currentGroup.key === group.key) { currentGroup.isCollapsed = false; } @@ -140,7 +182,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle props.onLoadMoreData(props.termSetId, Guid.parse(group.key), '', true) .then((loadedTerms) => { const grps: IGroup[] = loadedTerms.value.map(term => { - let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.languageTag && termLabel.isDefault === true)); if (termNames.length === 0) { termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); } @@ -159,10 +201,13 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle } return g; }); + setTerms((prevTerms) => { - const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); - return [...prevTerms, ...nonExistingTerms]; + const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); + const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); + return [...prevTerms, ...nonExistingTermsAsExt]; }); + group.children = grps; group.data.skiptoken = loadedTerms.skiptoken; group.hasMoreData = loadedTerms.skiptoken !== ''; @@ -172,7 +217,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle } else { setGroups((prevGroups) => { - const recurseGroups = (currentGroup) => { + const recurseGroups = (currentGroup: IGroup) => { if (currentGroup.key === group.key) { currentGroup.isCollapsed = true; } @@ -213,7 +258,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle const isSelected = selection.isKeySelected(groupHeaderProps.group.key); const selectionProps = { - "data-selection-index": selection.getItems().findIndex((term) => term.key === groupHeaderProps.group.key) + "data-selection-index": selection.getItems().findIndex((term) => term.id === groupHeaderProps.group.key) }; if (props.allowMultipleSelections) { @@ -270,7 +315,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle
@@ -301,25 +346,24 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle const onRenderFooter = (footerProps: IGroupFooterProps): JSX.Element => { if ((footerProps.group.hasMoreData || footerProps.group.children && footerProps.group.children.length === 0) && !footerProps.group.isCollapsed) { - const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.termStoreInfo.defaultLanguageTag; if (groupsLoading.some(value => value === footerProps.group.key)) { const spinnerStyles: IStyleFunctionOrObject = { circle: { verticalAlign: 'middle' } }; return ( -
+
); } const linkStyles: IStyleFunctionOrObject = { root: { fontSize: '14px', paddingLeft: (footerProps.groupLevel + 1) * 20 + 62 } }; return ( -
+
{ setGroupsLoading((prevGroupsLoading) => [...prevGroupsLoading, footerProps.group.key]); props.onLoadMoreData(props.termSetId, footerProps.group.key === props.termSetId.toString() ? Guid.empty : Guid.parse(footerProps.group.key), footerProps.group.data.skiptoken, true) .then((loadedTerms) => { const grps: IGroup[] = loadedTerms.value.map(term => { - let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === languageTag && termLabel.isDefault === true)); + let termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.languageTag && termLabel.isDefault === true)); if (termNames.length === 0) { termNames = term.labels.filter((termLabel) => (termLabel.languageTag === props.termStoreInfo.defaultLanguageTag && termLabel.isDefault === true)); } @@ -339,8 +383,9 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle return g; }); setTerms((prevTerms) => { - const nonExistingTerms = grps.filter((grp) => prevTerms.every((prevTerm) => prevTerm.key !== grp.key)); - return [...prevTerms, ...nonExistingTerms]; + const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); + const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); + return [...prevTerms, ...nonExistingTermsAsExt]; }); footerProps.group.children = [...footerProps.group.children, ...grps]; footerProps.group.data.skiptoken = loadedTerms.skiptoken; @@ -368,19 +413,14 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle onRenderShowAll: onRenderShowAll, }; - function getTagText(tag: ITag, currentValue?: string) { - return tag.name; - } - - const onPickerChange = (items?: ITag[]): void => { - selection.setAllSelected(false); - const itemsToAdd = items.filter((item) => terms.every((term) => term.key !== item.key)); - if (itemsToAdd.length > 0) { - selection.setItems([...terms, ...itemsToAdd]); - setTerms((prevTerms) => [...prevTerms, ...itemsToAdd]); - } + const onPickerChange = (items?: ITermInfoExt[]): void => { + const itemsToAdd = items.filter((item) => terms.every((term) => term.id !== item.id)); + setTerms((prevTerms) => [...prevTerms, ...itemsToAdd]); + selection.setItems([...selection.getItems(), ...itemsToAdd], true); for (const item of items) { - selection.setKeySelected(item.key.toString(), true, true); + if (selection.canSelectItem(item)) { + selection.setKeySelected(item.id.toString(), true, false); + } } }; @@ -390,18 +430,21 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle
-
diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItem.styles.ts b/src/controls/modernTaxonomyPicker/termItem/TermItem.styles.ts new file mode 100644 index 000000000..e9f13078d --- /dev/null +++ b/src/controls/modernTaxonomyPicker/termItem/TermItem.styles.ts @@ -0,0 +1,134 @@ +import { ButtonGlobalClassNames } from "office-ui-fabric-react/lib/components/Button/BaseButton.classNames"; +import { getFocusStyle, getGlobalClassNames, HighContrastSelector } from "office-ui-fabric-react/lib/Styling"; +import { getRTL } from "office-ui-fabric-react/lib/Utilities"; +import { ITermItemStyleProps, ITermItemStyles } from "../modernTermPicker/ModernTermPicker.types"; + +const GlobalClassNames = { + root: 'ms-TagItem', + text: 'ms-TagItem-text', + close: 'ms-TagItem-close', + isSelected: 'is-selected', +}; + +const TAG_HEIGHT = 26; + +export function getStyles(props: ITermItemStyleProps): ITermItemStyles { + const { className, theme, selected, disabled } = props; + + const { palette, effects, fonts, semanticColors } = theme; + + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + return { + root: [ + classNames.root, + fonts.medium, + getFocusStyle(theme), + { + boxSizing: 'content-box', + flexShrink: '1', + margin: 2, + height: TAG_HEIGHT, + lineHeight: TAG_HEIGHT, + cursor: 'default', + userSelect: 'none', + display: 'flex', + flexWrap: 'nowrap', + maxWidth: 300, + minWidth: 0, // needed to prevent long tags from overflowing container + borderRadius: effects.roundedCorner2, + color: semanticColors.inputText, + background: !selected || disabled ? palette.neutralLighter : palette.themePrimary, + selectors: { + ':hover': [ + !disabled && + !selected && { + color: palette.neutralDark, + background: palette.neutralLight, + selectors: { + '.ms-TagItem-close': { + color: palette.neutralPrimary, + }, + }, + }, + disabled && { background: palette.neutralLighter }, + selected && !disabled && { background: palette.themePrimary }, + ], + [HighContrastSelector]: { + border: `1px solid ${!selected ? 'WindowText' : 'WindowFrame'}`, + }, + }, + }, + disabled && { + selectors: { + [HighContrastSelector]: { + borderColor: 'GrayText', + }, + }, + }, + selected && + !disabled && [ + classNames.isSelected, + { + color: palette.white, + }, + ], + className, + ], + text: [ + classNames.text, + { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + minWidth: 30, + margin: '0 8px', + }, + disabled && { + selectors: { + [HighContrastSelector]: { + color: 'GrayText', + }, + }, + }, + ], + close: [ + classNames.close, + { + color: palette.neutralSecondary, + width: 30, + height: '100%', + flex: '0 0 auto', + borderRadius: getRTL(theme) + ? `${effects.roundedCorner2} 0 0 ${effects.roundedCorner2}` + : `0 ${effects.roundedCorner2} ${effects.roundedCorner2} 0`, + selectors: { + ':hover': { + background: palette.neutralQuaternaryAlt, + color: palette.neutralPrimary, + }, + ':active': { + color: palette.white, + backgroundColor: palette.themeDark, + }, + }, + }, + selected && { + color: palette.white, + selectors: { + ':hover': { + color: palette.white, + background: palette.themeDark, + }, + }, + }, + disabled && { + selectors: { + [`.${ButtonGlobalClassNames.msButtonIcon}`]: { + color: palette.neutralSecondary, + }, + }, + }, + ], + }; +} diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItem.tsx b/src/controls/modernTaxonomyPicker/termItem/TermItem.tsx new file mode 100644 index 000000000..18a465d0a --- /dev/null +++ b/src/controls/modernTaxonomyPicker/termItem/TermItem.tsx @@ -0,0 +1,64 @@ +import { IconButton } from 'office-ui-fabric-react/lib/components/Button/IconButton/IconButton'; +import { classNamesFunction, styled } from 'office-ui-fabric-react/lib/Utilities'; +import * as React from 'react'; +import { ITermItemProps, ITermItemStyleProps, ITermItemStyles } from '../modernTermPicker/ModernTermPicker.types'; +import { getStyles } from './TermItem.styles'; + +const getClassNames = classNamesFunction(); + +/** + * {@docCategory TagPicker} + */ +export const TermItemBase = (props: ITermItemProps) => { + const { + theme, + styles, + selected, + disabled, + enableTermFocusInDisabledPicker, + children, + className, + index, + onRemoveItem, + removeButtonAriaLabel, + termStoreInfo, + languageTag, + } = props; + + const classNames = getClassNames(styles, { + theme: theme!, + className, + selected, + disabled, + }); + + let labels = props.item.labels.filter((name) => name.languageTag === languageTag && name.isDefault); + if (labels.length === 0) { + labels = props.item.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); + } + + return ( +
+ + {children} + + +
+ ); +}; + +export const TermItem = styled(TermItemBase, getStyles, undefined, { + scope: 'TermItem', +}); diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.styles.ts b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.styles.ts new file mode 100644 index 000000000..16a07d981 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.styles.ts @@ -0,0 +1,26 @@ +import { getGlobalClassNames } from "office-ui-fabric-react/lib/Styling"; +import { ITermItemSuggestionStyleProps, ITermItemSuggestionStyles } from "../modernTermPicker/ModernTermPicker.types"; + +const GlobalClassNames = { + suggestionTextOverflow: 'ms-TagItem-TextOverflow', +}; + +export function getStyles(props: ITermItemSuggestionStyleProps): ITermItemSuggestionStyles { + const { className, theme } = props; + + const classNames = getGlobalClassNames(GlobalClassNames, theme); + + return { + suggestionTextOverflow: [ + classNames.suggestionTextOverflow, + { + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '60vw', + padding: '6px 12px 7px', + whiteSpace: 'nowrap', + }, + className, + ], + }; +} diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx new file mode 100644 index 000000000..75e585998 --- /dev/null +++ b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx @@ -0,0 +1,39 @@ +import React, { useEffect } from "react"; +import { ISuggestionItemProps } from "office-ui-fabric-react"; +import styles from './TermItemSuggestions.module.scss'; +import * as strings from 'ControlStrings'; +import { Guid } from "@microsoft/sp-core-library"; +import { ITermStoreInfo } from "@pnp/sp/taxonomy"; +import { ITermInfoExt } from "../modernTermPicker/ModernTermPicker.types"; + +export interface ITermItemSuggestionProps extends ISuggestionItemProps { + term: ITermInfoExt; + languageTag: string; + termStoreInfo: ITermStoreInfo; + onLoadParentLabel: (termId: Guid) => Promise; +} + +export function TermItemSuggestion(props: ITermItemSuggestionProps): JSX.Element { + const [parentLabel, setParentLabel] = React.useState(""); + + useEffect(() => { + props.onLoadParentLabel(Guid.parse(props.term.id.toString())) + .then((localParentInfo) => { + setParentLabel(localParentInfo); + }); + }, []); + + let labels = props.term.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault); + if (labels.length === 0) { + labels = props.term.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); + } + + return ( +
+ {labels[0].name} + {parentLabel !== "" &&
+ {`${strings.ModernTaxonomyPickerSuggestionInLabel} ${parentLabel}`} +
} +
+ ); +} diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestions.module.scss b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestions.module.scss new file mode 100644 index 000000000..b94f5cefc --- /dev/null +++ b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestions.module.scss @@ -0,0 +1,26 @@ +@import '~office-ui-fabric-react/dist/sass/References.scss'; + +html[dir='ltr'] .termSuggestionContainer +{ + text-align: left; +} + +html[dir='rtl'] .termSuggestionContainer +{ + text-align: right; +} + +.termSuggestionContainer +{ + padding-top: 7px; + padding-left: 12px; + padding-right: 12px; + padding-bottom: 7px; + + .termSuggestionPath + { + font-size: 12px; + color: #666666; + } + +} diff --git a/src/services/SPTaxonomyService.ts b/src/services/SPTaxonomyService.ts index d6d03e016..5e9a6dd57 100644 --- a/src/services/SPTaxonomyService.ts +++ b/src/services/SPTaxonomyService.ts @@ -7,9 +7,6 @@ import { ITermInfo, ITermSetInfo, ITermStoreInfo } from '@pnp/sp/taxonomy'; export class SPTaxonomyService { - /** - * Service constructor - */ constructor(private context: BaseComponentContext) { } @@ -54,7 +51,7 @@ export class SPTaxonomyService { return undefined; } try { - const termInfo = await sp.termStore.sets.getById(termSetId.toString()).terms.getById(termId.toString())(); + const termInfo = await sp.termStore.sets.getById(termSetId.toString()).terms.getById(termId.toString()).expand("parent")(); return termInfo; } catch (error) { return undefined; From 5e66e8ad8ee1bc412898e92b198670b4ce57f30f Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Mon, 20 Sep 2021 10:25:29 +0200 Subject: [PATCH 13/19] Updated docs with ModernTaxonomyPicker --- .../assets/modernTaxonomyPicker-empty.png | Bin 0 -> 1952 bytes ...odernTaxonomyPicker-input-autocomplete.png | Bin 0 -> 7628 bytes .../modernTaxonomyPicker-selected-terms.png | Bin 0 -> 3697 bytes .../modernTaxonomyPicker-tree-selection.png | Bin 0 -> 16552 bytes .../docs/controls/ModernTaxonomyPicker.md | 74 ++++++++++++++++++ docs/documentation/docs/index.md | 1 + docs/documentation/mkdocs.yml | 1 + 7 files changed, 76 insertions(+) create mode 100644 docs/documentation/docs/assets/modernTaxonomyPicker-empty.png create mode 100644 docs/documentation/docs/assets/modernTaxonomyPicker-input-autocomplete.png create mode 100644 docs/documentation/docs/assets/modernTaxonomyPicker-selected-terms.png create mode 100644 docs/documentation/docs/assets/modernTaxonomyPicker-tree-selection.png create mode 100644 docs/documentation/docs/controls/ModernTaxonomyPicker.md diff --git a/docs/documentation/docs/assets/modernTaxonomyPicker-empty.png b/docs/documentation/docs/assets/modernTaxonomyPicker-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..59424570e5c4d7aebc40588fa2c698f3830ce842 GIT binary patch literal 1952 zcmYjR3sjP68m8OUWHRGzJC)tE*~Xd_a0YQS^LmWDLK?X#+G@F=qId(>qBJf2F&gh{ zilT{*(9(c6Ol7RaanxB2yyPXxn9vM#?avHv{rltWp0n>c-}k-O=e?Zwe7S-C-p0H4 z??xaH#`v?IL%+Z`bqu$+U1?a^+2ZU}_)3XUE991x;1vTASL^PHu10hhx zbU@+A`Q0k5Mj>DkP>>!R6Gmdtk-{+$1oe6l?9PI_$m%voaG5}i@OOYfi~|D(P=SzE z3YU~11>urF83To(wqnp>LJg$v&XU42p|`>oCOjFbR+*2*pczsPj0I}Mk)SjI!)S#i zNZT6YfFiO4N`pzG6azv9JVgN+4HwMqS7}uJTowV3LF2Qfs^@&JwG-0Z2c_k8*Zl$RLf3MS`;L6aPaLyzMb~Pq!c@dA@wD%J+(8Pt_D*v?k~)eA}_C zWP*)Vt0?E26Rn{VFMbVMWL_NeJY_tkVL5Q3x$#fLzwc{i+lp+0YTLo^r43fq>_bQU z=&L;^D;!rl!XG&fZmchM%&uLpT)AuxC;uPSA8~BkQZq`ae~vwPPBYS5DxvWzi`+Nt z2Z|(5LYiy0A1W+NUv@M;fTdnrNb~azD3HD{C-d%yF$qb*KY6*xuNXPDow`&e@T-xs zFNIt2_qc|ZM97;dll~1plZ-da5VLnfb6}SRiAql^I(Mn1PncwJd?eLM^nN5GMx;Sc z?_13i_NezW+7?ztQ?v;BRQijtMpFDKQNYV`nk?HXzH@$=wGr~-CXKPjGI=&4(yI=fq4)@u3brA2GA!`F56WqS%} z7U1%s$z*Q@_-mu(ZECD`wsO|qR{xZ3R_8xN3Ya!|*V^ZA*Xexy=aZ%vx#8ST6Ta58 zQC*j%i33BWjsH=4HC|@%Mvt{K{BFM!t~jke!DX>>htb`8c_BejC%Sxq-F{z2&Xk)n z`_bt)r8aNgQk#ohX!TxGUx!jvlHk|Af^sddq zIQSZYQhZ0?%Q7bYd35F!Fd8y}^5c^h3n$lQ_lKcJ_%BHFesLR#bENPak^AX5Ieyry zrs-drAUhJez|{KSp7?gk9l=@Z57hT12^!GC`Cfhw`&m|CF*0oBkFgJS4DYQjmFE$r zZ=Zibh@WKWf?{4Q97ob)vwaeer<4Rg_NjLjmABy3cg_~}rj~QdkWRtMn+x7@{*$A$ zkF71Z<&s?7#OuXF=a?NIUY&kPU>!?FC|Z(auTz_G!S0cCb?qRBz-8FnqABZ++8!Lp zY4q^#Xt;m=%Xpd`X~n_K`+lD@G+(4%a{7z6HRsGXN5+Q+8oKUor%zNYFK?`NJ|1+- zlrFk(3U|G9{cGb2Q*xPJ$0dd;_w_v4`=GRVI!~D4NspzieLBoqr+mz~btZZ-iPx0( zttXN|d>uFAQ`1(FbrOG*)($*C-|sbRa@gNgQxSTjIyY2(p^S5kF3>-J$%1|x{0Rwd z+?z`%_|a9{7-mUY5@tB{s)Nw8y+y~qo=#u;%(RDMUi;K5^lz3%dd9xeo-oVkK&DU6 z4YQrj+x~xZIO85iKP}}Oo1a1z=Y>drSy$|2;$NBkwz-O3(O-SAJ0$6CZ|e9*nPD;U MUjCjU_lV5@0HF#b)c^nh literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/assets/modernTaxonomyPicker-input-autocomplete.png b/docs/documentation/docs/assets/modernTaxonomyPicker-input-autocomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..ba230da1daaebeebcc35fc9d808f66bb62393e5c GIT binary patch literal 7628 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 literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/assets/modernTaxonomyPicker-selected-terms.png b/docs/documentation/docs/assets/modernTaxonomyPicker-selected-terms.png new file mode 100644 index 0000000000000000000000000000000000000000..68943ff1f01e3d39aaf448c74f2baf0571d10003 GIT binary patch literal 3697 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 F_8-C8MDPFr literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/assets/modernTaxonomyPicker-tree-selection.png b/docs/documentation/docs/assets/modernTaxonomyPicker-tree-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..cb843ad2c2264c736ddb0b1e642c23667996839a GIT binary patch literal 16552 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 S{_Ecb0E)6|GF4K>LH`eUF@stF literal 0 HcmV?d00001 diff --git a/docs/documentation/docs/controls/ModernTaxonomyPicker.md b/docs/documentation/docs/controls/ModernTaxonomyPicker.md new file mode 100644 index 000000000..0836c590e --- /dev/null +++ b/docs/documentation/docs/controls/ModernTaxonomyPicker.md @@ -0,0 +1,74 @@ +# Modern Taxonomy Picker + +This control allows you to select one or more Terms from a TermSet via its TermSet ID. You can also configure the control to select the child terms from a specific term in the TermSet by setting the anchorTermId. This is the modern version of the taxonomy picker that uses the REST API and makes use of some load on demand features which makes it well suited for large term sets. + +!!! note "Disclaimer" + Since this control is meant to look as and work in the same way as the out-of-the-box control it lacks some of the features from the legacy ```TaxonomyPicker``` control. If you need some of those features please continue using the legacy version. + +**Empty term picker** + +![Empty term picker](../assets/modernTaxonomyPicker-empty.png) + +**Selecting terms** + +![Selecting terms](../assets/modernTaxonomyPicker-tree-selection.png) + +**Selected terms in picker** + +![Selected terms in the input](../assets/modernTaxonomyPicker-selected-terms.png) + +**Term picker: Auto Complete** + +![Selected terms in the input](../assets/modernTaxonomyPicker-input-autocomplete.png) + + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency. +- Import the following modules to your component: + +```TypeScript +import { ModernTaxonomyPicker } from "@pnp/spfx-controls-react/lib/ModernTaxonomyPicker"; +``` + +- Use the `ModernTaxonomyPicker` control in your code as follows: + +```TypeScript + +``` + +- With the `onChange` property you can capture the event of when the terms in the picker has changed: + +```typescript +private onTaxPickerChange(terms : ITermInfo[]) { + console.log("Terms", terms); +} +``` + +## Implementation + +The ModernTaxonomyPicker control can be configured with the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| panelTitle | string | yes | TermSet Picker Panel title. | +| label | string | yes | Text displayed above the Taxonomy Picker. | +| disabled | boolean | no | Specify if the control should be disabled. Default value is false. | +| context | BaseComponentContext | yes | Context of the current web part or extension. | +| initialValues | ITermInfo[] | no | Defines the terms selected by default. ITermInfo comes from PnP/PnPjs and can be imported with
```import { ITermInfo } from '@pnp/sp/taxonomy';``` | +| allowMultipleSelections | boolean | no | Defines if the user can select only one or multiple terms. Default value is false. | +| termSetId | string | yes | The Id of the TermSet that you would like the Taxonomy Picker to select terms from. | +| onChange | function | no | Captures the event of when the terms in the picker has changed. | +| anchorTermId | string | no | Set the id of a child term in the TermSet to be able to select terms from that level and below. | +| placeHolder | string | no | Short text hint to display in picker. | +| required | boolean | no | Specifies if to display an asterisk near the label. Default value is false. | +| customPanelWidth | number | no | Custom panel width in pixels. | +| onRenderItem | function | no | Modify the display of the items in the picker. | +| onRenderSuggestionsItem | function | no | Modify the display of the items in the pickers suggestions list. | + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/TaxonomyPicker) diff --git a/docs/documentation/docs/index.md b/docs/documentation/docs/index.md index d397e2d3b..3d83a975c 100644 --- a/docs/documentation/docs/index.md +++ b/docs/documentation/docs/index.md @@ -84,6 +84,7 @@ The following controls are currently available: - [LivePersona](./controls/LivePersona) (Live Persona control) - [LocationPicker](./controls/LocationPicker) (Location Picker control) - [Map](./controls/Map) (renders a map in a web part) +- [ModernTaxonomyPicker](./controls/ModernTaxonomyPicker) (Modern Taxonomy Picker) - [MyTeams](./controls/MyTeams) (My Teams) - [PeoplePicker](./controls/PeoplePicker) (People Picker) - [Placeholder](./controls/Placeholder) (shows an initial placeholder if the web part has to be configured) diff --git a/docs/documentation/mkdocs.yml b/docs/documentation/mkdocs.yml index 4b7e5b2a1..b2b59a331 100644 --- a/docs/documentation/mkdocs.yml +++ b/docs/documentation/mkdocs.yml @@ -46,6 +46,7 @@ nav: - LivePersona: 'controls/LivePersona.md' - LocationPicker: 'controls/LocationPicker.md' - Map: 'controls/Map.md' + - ModernTaxonomyPicker: 'controls/ModernTaxonomyPicker.md' - MyTeams: 'controls/MyTeams.md' - Pagination: 'controls/Pagination.md' - PeoplePicker: 'controls/PeoplePicker.md' From 686764add32ff1272d980c1a6e62f94a724ed687 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Mon, 20 Sep 2021 10:29:30 +0200 Subject: [PATCH 14/19] Added ModernTaxonomyPicker to ControlsTest --- .../controlsTest/ControlsTestWebPart.ts | 1 + .../controlsTest/components/ControlsTest.tsx | 12 +++++++++ .../ControlsTest_SingleComponent.tsx | 25 +++++++++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/webparts/controlsTest/ControlsTestWebPart.ts b/src/webparts/controlsTest/ControlsTestWebPart.ts index 148cca944..6db036a85 100644 --- a/src/webparts/controlsTest/ControlsTestWebPart.ts +++ b/src/webparts/controlsTest/ControlsTestWebPart.ts @@ -19,6 +19,7 @@ import { ThemeProvider, } from "@microsoft/sp-component-base"; import ControlsTest from './components/ControlsTest'; +import ControlsTest_SingleComponent from './components/ControlsTest_SingleComponent'; /** * Web part to test the React controls */ diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index d7aaa36d5..88c4b3410 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -178,6 +178,7 @@ import { DynamicForm } from '../../../controls/dynamicForm'; import { LocationPicker } from "../../../controls/locationPicker/LocationPicker"; import { ILocationPickerItem } from "../../../controls/locationPicker/ILocationPicker"; import { debounce } from "lodash"; +import { ModernTaxonomyPicker } from "../../../controls/modernTaxonomyPicker/ModernTaxonomyPicker"; @@ -1926,6 +1927,17 @@ export default class ControlsTest extends React.Component { console.log(locValue.DisplayName + ", " + locValue.Address.Street); }}> + +
diff --git a/src/webparts/controlsTest/components/ControlsTest_SingleComponent.tsx b/src/webparts/controlsTest/components/ControlsTest_SingleComponent.tsx index 1e4e1d0a1..44ca82683 100644 --- a/src/webparts/controlsTest/components/ControlsTest_SingleComponent.tsx +++ b/src/webparts/controlsTest/components/ControlsTest_SingleComponent.tsx @@ -57,6 +57,7 @@ import { ImageFit } from 'office-ui-fabric-react/lib/Image'; import { FilePicker, IFilePickerResult } from '../../../FilePicker'; import { FolderExplorer, IFolder, IBreadcrumbItem } from '../../../FolderExplorer'; import { Pagination } from '../../../controls/pagination'; +import { ModernTaxonomyPicker } from '../../../ModernTaxonomyPicker'; /** * The sample data below was randomly generated (except for the title). It is used by the grid layout @@ -459,16 +460,20 @@ export default class ControlsTest extends React.Component - { console.log('Cancelled'); }} - onSubmitted={async (listItem) => { console.log(listItem); }}> - - +
+ alert(values.map((value) => `${value?.id} - ${value?.labels[0].name}`).join("\n"))} + disabled={false} + customPanelWidth={400} + /> +
); } From 2ae2c6fbcf6f1033fce344443cef5b3a6de37403 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 21 Sep 2021 19:43:32 +0200 Subject: [PATCH 15/19] Removed need for ITermInfoExt --- .../ModernTaxonomyPicker.tsx | 28 ++++++-------- .../modernTermPicker/ModernTermPicker.tsx | 12 ++---- .../ModernTermPicker.types.ts | 9 +---- .../TaxonomyPanelContents.tsx | 28 ++++++-------- .../termItem/TermItemSuggestion.tsx | 38 ++++++++++++------- 5 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index e477444b7..7b014657a 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -33,9 +33,7 @@ import { ITermInfo, } from '@pnp/sp/taxonomy'; import { TermItemSuggestion } from './termItem/TermItemSuggestion'; import { ModernTermPicker } from './modernTermPicker/ModernTermPicker'; -import { ITermInfoExt, - ITermItemProps - } from './modernTermPicker/ModernTermPicker.types'; +import { ITermItemProps } from './modernTermPicker/ModernTermPicker.types'; import { TermItem } from './termItem/TermItem'; export interface IModernTaxonomyPickerProps { @@ -50,7 +48,7 @@ export interface IModernTaxonomyPickerProps { required?: boolean; onChange?: (newValue?: ITermInfo[]) => void; onRenderItem?: (itemProps: ITermItemProps) => JSX.Element; - onRenderSuggestionsItem?: (term: ITermInfoExt, itemProps: ISuggestionItemProps) => JSX.Element; + onRenderSuggestionsItem?: (term: ITermInfo, itemProps: ISuggestionItemProps) => JSX.Element; placeHolder?: string; customPanelWidth?: number; } @@ -58,8 +56,8 @@ export interface IModernTaxonomyPickerProps { export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const [taxonomyService] = React.useState(() => new SPTaxonomyService(props.context)); const [panelIsOpen, setPanelIsOpen] = React.useState(false); - const [selectedOptions, setSelectedOptions] = React.useState([]); - const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); + const [selectedOptions, setSelectedOptions] = React.useState([]); + const [selectedPanelOptions, setSelectedPanelOptions] = React.useState([]); const [currentTermStoreInfo, setCurrentTermStoreInfo] = React.useState(); const [currentTermSetInfo, setCurrentTermSetInfo] = React.useState(); const [currentAnchorTermInfo, setCurrentAnchorTermInfo] = React.useState(); @@ -74,7 +72,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { 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;}) : + props.initialValues.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfo;}) : []); }); taxonomyService.getTermSetInfo(Guid.parse(props.termSetId)) @@ -91,10 +89,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { React.useEffect(() => { if (props.onChange) { - props.onChange(selectedOptions.map(termInfoExt => { - const {languageTag, termStoreInfo, ...termInfo} = termInfoExt; - return termInfo; - })); + props.onChange(selectedOptions); } }, [selectedOptions]); @@ -116,7 +111,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { onClosePanel(); } - async function onResolveSuggestions(filter: string, selectedItems?: ITermInfoExt[]): Promise { + async function onResolveSuggestions(filter: string, selectedItems?: ITermInfo[]): Promise { if (filter === '') { return []; } @@ -128,8 +123,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { return selectedItems.every((item) => item.id !== term.id); }); const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable); - const filteredTermsAndAvailableAsExt = filteredTermsAndAvailable.map(term => { return { ...term, languageTag: currentLanguageTag, termStoreInfo: currentTermStoreInfo } as ITermInfoExt;}); - return filteredTermsAndAvailableAsExt; + return filteredTermsAndAvailable; } async function onLoadParentLabel(termId: Guid): Promise { @@ -150,7 +144,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { } } - function onRenderSuggestionsItem(term: ITermInfoExt, itemProps: ISuggestionItemProps): JSX.Element { + function onRenderSuggestionsItem(term: ITermInfo, itemProps: ISuggestionItemProps): JSX.Element { return ( (termLabel.languageTag === currentLanguageTag)); if (labelsWithMatchingLanguageTag.length === 0) { labelsWithMatchingLanguageTag = termInfo.labels.filter((termLabel) => (termLabel.languageTag === currentTermStoreInfo.defaultLanguageTag)); @@ -199,7 +193,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { selectedItems={selectedOptions} disabled={props.disabled} styles={tagPickerStyles} - onChange={(itms?: ITermInfoExt[]) => { + onChange={(itms?: ITermInfo[]) => { setSelectedOptions(itms || []); setSelectedPanelOptions(itms || []); }} diff --git a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx index 34cf8a0f9..fdef4a5c0 100644 --- a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx +++ b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.tsx @@ -1,7 +1,6 @@ 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"; @@ -14,9 +13,9 @@ 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"; +import { ITermInfo } from "@pnp/sp/taxonomy"; -export class ModernTermPickerBase extends BasePicker { +export class ModernTermPickerBase extends BasePicker { public static defaultProps = { onRenderItem: (props: ITermItemProps) => { let labels = props.item.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault); @@ -28,11 +27,8 @@ export class ModernTermPickerBase extends BasePicker{labels[0].name} ) : null; }, - onRenderSuggestionsItem: (props: ITermInfoExt, itemProps: ISuggestionItemProps) => { - const onLoadParentLabel = async (termId: Guid): Promise => { - return Promise.resolve(""); - }; - return ; + onRenderSuggestionsItem: (props: ITermInfo, itemProps: ISuggestionItemProps) => { + return ; }, }; diff --git a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts index 679b597fa..abbb6d41e 100644 --- a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts +++ b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts @@ -4,14 +4,9 @@ import { IPickerItemProps } from "office-ui-fabric-react/lib/components/pickers/ 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 {} +export interface IModernTermPickerProps extends IBasePickerProps {} -export interface ITermItemProps extends IPickerItemProps { +export interface ITermItemProps extends IPickerItemProps { /** Additional CSS class(es) to apply to the TermItem root element. */ className?: string; diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index 2b9aa6a0d..d9d9a5da4 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -46,31 +46,30 @@ import { css } from '@uifabric/utilities/lib/css'; import * as strings from 'ControlStrings'; import { useForceUpdate } from '@uifabric/react-hooks'; import { ModernTermPicker } from '../modernTermPicker/ModernTermPicker'; -import { ITermInfoExt } from '../modernTermPicker/ModernTermPicker.types'; export interface ITaxonomyFormProps { context: BaseComponentContext; allowMultipleSelections?: boolean; termSetId: Guid; pageSize: number; - selectedPanelOptions: ITermInfoExt[]; - setSelectedPanelOptions: React.Dispatch>; - onResolveSuggestions: (filter: string, selectedItems?: ITermInfoExt[]) => ITermInfoExt[] | PromiseLike; + selectedPanelOptions: ITermInfo[]; + setSelectedPanelOptions: React.Dispatch>; + onResolveSuggestions: (filter: string, selectedItems?: ITermInfo[]) => ITermInfo[] | PromiseLike; onLoadMoreData: (termSetId: Guid, parentTermId?: Guid, skiptoken?: string, hideDeprecatedTerms?: boolean, pageSize?: number) => Promise<{ value: ITermInfo[], skiptoken: string }>; anchorTermInfo: ITermInfo; termSetInfo: ITermSetInfo; termStoreInfo: ITermStoreInfo; placeHolder: string; - onRenderSuggestionsItem?: (props: ITermInfoExt, itemProps: ISuggestionItemProps) => JSX.Element; - onRenderItem?: (props: IPickerItemProps) => JSX.Element; - getTextFromItem: (item: ITermInfoExt, currentValue?: string) => string; + onRenderSuggestionsItem?: (props: ITermInfo, itemProps: ISuggestionItemProps) => JSX.Element; + onRenderItem?: (props: IPickerItemProps) => JSX.Element; + getTextFromItem: (item: ITermInfo, currentValue?: string) => string; languageTag: string; } export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactElement { const [groupsLoading, setGroupsLoading] = React.useState([]); const [groups, setGroups] = React.useState([]); - const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); + const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); const forceUpdate = useForceUpdate(); @@ -78,7 +77,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle const s = new Selection({onSelectionChanged: () => { props.setSelectedPanelOptions((prevOptions) => [...selection.getSelection()]); forceUpdate(); - }, getKey: (term: ITermInfoExt) => term.id}); + }, getKey: (term: any) => term.id}); s.setItems(terms); for (const selectedOption of props.selectedPanelOptions) { if (s.canSelectItem) { @@ -141,8 +140,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle }); setTerms((prevTerms) => { const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); - const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); - return [...prevTerms, ...nonExistingTermsAsExt]; + return [...prevTerms, ...nonExistingTerms]; }); rootGroup.children = grps; rootGroup.data.skiptoken = loadedTerms.skiptoken; @@ -204,8 +202,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle setTerms((prevTerms) => { const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); - const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); - return [...prevTerms, ...nonExistingTermsAsExt]; + return [...prevTerms, ...nonExistingTerms]; }); group.children = grps; @@ -384,8 +381,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle }); setTerms((prevTerms) => { const nonExistingTerms = loadedTerms.value.filter((term) => prevTerms.every((prevTerm) => prevTerm.id !== term.id)); - const nonExistingTermsAsExt = nonExistingTerms.map(term => { return { ...term, languageTag: props.languageTag, termStoreInfo: props.termStoreInfo } as ITermInfoExt;}); - return [...prevTerms, ...nonExistingTermsAsExt]; + return [...prevTerms, ...nonExistingTerms]; }); footerProps.group.children = [...footerProps.group.children, ...grps]; footerProps.group.data.skiptoken = loadedTerms.skiptoken; @@ -413,7 +409,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle onRenderShowAll: onRenderShowAll, }; - const onPickerChange = (items?: ITermInfoExt[]): void => { + const onPickerChange = (items?: ITermInfo[]): void => { const itemsToAdd = items.filter((item) => terms.every((term) => term.id !== item.id)); setTerms((prevTerms) => [...prevTerms, ...itemsToAdd]); selection.setItems([...selection.getItems(), ...itemsToAdd], true); diff --git a/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx index 75e585998..daed71b5e 100644 --- a/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx +++ b/src/controls/modernTaxonomyPicker/termItem/TermItemSuggestion.tsx @@ -3,34 +3,46 @@ import { ISuggestionItemProps } from "office-ui-fabric-react"; import styles from './TermItemSuggestions.module.scss'; import * as strings from 'ControlStrings'; import { Guid } from "@microsoft/sp-core-library"; -import { ITermStoreInfo } from "@pnp/sp/taxonomy"; -import { ITermInfoExt } from "../modernTermPicker/ModernTermPicker.types"; +import { ITermInfo, ITermStoreInfo } from "@pnp/sp/taxonomy"; export interface ITermItemSuggestionProps extends ISuggestionItemProps { - term: ITermInfoExt; - languageTag: string; - termStoreInfo: ITermStoreInfo; - onLoadParentLabel: (termId: Guid) => Promise; + term: ITermInfo; + languageTag?: string; + termStoreInfo?: ITermStoreInfo; + onLoadParentLabel?: (termId: Guid) => Promise; } -export function TermItemSuggestion(props: ITermItemSuggestionProps): JSX.Element { +export function TermItemSuggestion(props: ITermItemSuggestionProps): JSX.Element { const [parentLabel, setParentLabel] = React.useState(""); useEffect(() => { - props.onLoadParentLabel(Guid.parse(props.term.id.toString())) + if (props.onLoadParentLabel) { + props.onLoadParentLabel(Guid.parse(props.term.id.toString())) .then((localParentInfo) => { setParentLabel(localParentInfo); }); + } }, []); - let labels = props.term.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault); - if (labels.length === 0) { - labels = props.term.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); + let labels: { + name: string; + isDefault: boolean; + languageTag: string; + }[]; + + if (props.languageTag && props.termStoreInfo) { + labels = props.term.labels.filter((name) => name.languageTag === props.languageTag && name.isDefault); + if (labels.length === 0) { + labels = props.term.labels.filter((name) => name.languageTag === props.termStoreInfo.defaultLanguageTag && name.isDefault); + } + } + else { + labels = props.term.labels.filter((name) => name.isDefault); } return ( -
- {labels[0].name} +
+ {labels[0]?.name} {parentLabel !== "" &&
{`${strings.ModernTaxonomyPickerSuggestionInLabel} ${parentLabel}`}
} From f5ba2fa29d7531e2d3c3280f48661df142436758 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 21 Sep 2021 23:34:46 +0200 Subject: [PATCH 16/19] Additional styling and theme support --- .../docs/controls/ModernTaxonomyPicker.md | 1 + .../modernTaxonomyPicker/ModernTaxonomyPicker.tsx | 4 ++++ .../modernTermPicker/ModernTermPicker.types.ts | 5 ++++- .../TaxonomyPanelContents.module.scss | 12 ++++++++++++ .../taxonomyPanelContents/TaxonomyPanelContents.tsx | 12 ++++++++---- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/documentation/docs/controls/ModernTaxonomyPicker.md b/docs/documentation/docs/controls/ModernTaxonomyPicker.md index 0836c590e..b2f10b760 100644 --- a/docs/documentation/docs/controls/ModernTaxonomyPicker.md +++ b/docs/documentation/docs/controls/ModernTaxonomyPicker.md @@ -70,5 +70,6 @@ The ModernTaxonomyPicker control can be configured with the following properties | customPanelWidth | number | no | Custom panel width in pixels. | | onRenderItem | function | no | Modify the display of the items in the picker. | | onRenderSuggestionsItem | function | no | Modify the display of the items in the pickers suggestions list. | +| themeVariant | IReadonlyTheme | no | The current loaded SharePoint theme/section background (More info: [Supporting section backgrounds](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/supporting-section-backgrounds)). | ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/TaxonomyPicker) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 7b014657a..3799deed4 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -35,6 +35,7 @@ import { TermItemSuggestion } from './termItem/TermItemSuggestion'; import { ModernTermPicker } from './modernTermPicker/ModernTermPicker'; import { ITermItemProps } from './modernTermPicker/ModernTermPicker.types'; import { TermItem } from './termItem/TermItem'; +import { IReadonlyTheme } from "@microsoft/sp-component-base"; export interface IModernTaxonomyPickerProps { allowMultipleSelections?: boolean; @@ -51,6 +52,7 @@ export interface IModernTaxonomyPickerProps { onRenderSuggestionsItem?: (term: ITermInfo, itemProps: ISuggestionItemProps) => JSX.Element; placeHolder?: string; customPanelWidth?: number; + themeVariant?: IReadonlyTheme; } export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { @@ -205,6 +207,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { }} onRenderSuggestionsItem={props.onRenderSuggestionsItem ?? onRenderSuggestionsItem} onRenderItem={props.onRenderItem ?? onRenderItem} + themeVariant={props.themeVariant} />
@@ -260,6 +263,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { onRenderItem={props.onRenderItem ?? onRenderItem} getTextFromItem={getTextFromItem} languageTag={currentLanguageTag} + themeVariant={props.themeVariant} />
) diff --git a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts index abbb6d41e..d0d770463 100644 --- a/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts +++ b/src/controls/modernTaxonomyPicker/modernTermPicker/ModernTermPicker.types.ts @@ -3,8 +3,11 @@ import { IBasePickerProps } from "office-ui-fabric-react/lib/components/pickers/ 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"; +import { IReadonlyTheme } from "@microsoft/sp-component-base"; -export interface IModernTermPickerProps extends IBasePickerProps {} +export interface IModernTermPickerProps extends IBasePickerProps { + themeVariant?: IReadonlyTheme; +} export interface ITermItemProps extends IPickerItemProps { /** Additional CSS class(es) to apply to the TermItem root element. */ diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss index 3be7fc38c..a3085efda 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.module.scss @@ -3,6 +3,13 @@ .taxonomyPanelContents { .choiceOption { color: "[theme: bodyText, default: #323130]"; + display: inline-block; + padding-left: 26px; + } + + .disabledChoiceOption { + color: "[theme: disabledBodyText, default: #323130]"; + display: inline-block; padding-left: 26px; } @@ -15,6 +22,11 @@ margin-left: 4px; } + .disabledCheckbox { + color: "[theme: disabledBodyText, default: #323130]"; + margin-left: 4px; + } + .selectedCheckbox { font-weight: bold; } diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index d9d9a5da4..2f3d9b16a 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -46,6 +46,7 @@ import { css } from '@uifabric/utilities/lib/css'; import * as strings from 'ControlStrings'; import { useForceUpdate } from '@uifabric/react-hooks'; import { ModernTermPicker } from '../modernTermPicker/ModernTermPicker'; +import { IReadonlyTheme } from "@microsoft/sp-component-base"; export interface ITaxonomyFormProps { context: BaseComponentContext; @@ -64,6 +65,7 @@ export interface ITaxonomyFormProps { onRenderItem?: (props: IPickerItemProps) => JSX.Element; getTextFromItem: (item: ITermInfo, currentValue?: string) => string; languageTag: string; + themeVariant?: IReadonlyTheme; } export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactElement { @@ -282,7 +284,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle checked={isSelected} styles={selectedStyles} disabled={isDisabled} - onRenderLabel={(p) => + onRenderLabel={(p) => {p.label} } /> @@ -296,7 +298,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle text: groupHeaderProps.group.name, styles: selectedStyle, onRenderLabel: (p) => - + {p.text} }]; @@ -322,8 +324,8 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle const onRenderHeader = (headerProps: IGroupHeaderProps): JSX.Element => { const groupHeaderStyles: IStyleFunctionOrObject = { - expand: { height: 42, visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" }, - expandIsCollapsed: { visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible" }, + expand: { height: 42, visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible", fontSize: 14 }, + expandIsCollapsed: { visibility: !headerProps.group.children || headerProps.group.level === 0 ? "hidden" : "visible", fontSize: 14 }, check: { display: 'none' }, headerCount: { display: 'none' }, groupHeaderContainer: { height: 36, paddingTop: 3, paddingBottom: 3, paddingLeft: 3, paddingRight: 3, alignItems: 'center', }, @@ -337,6 +339,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle onRenderTitle={onRenderTitle} onToggleCollapse={onToggleCollapse} indentWidth={20} + expandButtonProps={{style: {color: props.themeVariant?.semanticColors.bodyText}}} /> ); }; @@ -441,6 +444,7 @@ export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactEle }} onRenderSuggestionsItem={props.onRenderSuggestionsItem} onRenderItem={props.onRenderItem} + themeVariant={props.themeVariant} />
From 57794c4b2cee0100bb0a658f5e6119a5dcd98af1 Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Tue, 21 Sep 2021 23:55:53 +0200 Subject: [PATCH 17/19] Renamed interface --- .../taxonomyPanelContents/TaxonomyPanelContents.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index 2f3d9b16a..0c4b58bf6 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -48,7 +48,7 @@ import { useForceUpdate } from '@uifabric/react-hooks'; import { ModernTermPicker } from '../modernTermPicker/ModernTermPicker'; import { IReadonlyTheme } from "@microsoft/sp-component-base"; -export interface ITaxonomyFormProps { +export interface ITaxonomyPanelContentsProps { context: BaseComponentContext; allowMultipleSelections?: boolean; termSetId: Guid; @@ -68,7 +68,7 @@ export interface ITaxonomyFormProps { themeVariant?: IReadonlyTheme; } -export function TaxonomyPanelContents(props: ITaxonomyFormProps): React.ReactElement { +export function TaxonomyPanelContents(props: ITaxonomyPanelContentsProps): React.ReactElement { const [groupsLoading, setGroupsLoading] = React.useState([]); const [groups, setGroups] = React.useState([]); const [terms, setTerms] = React.useState(props.selectedPanelOptions?.length > 0 ? [...props.selectedPanelOptions] : []); From 76ded8011142a3a7087cd487f9d080c0753ff6de Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Thu, 23 Sep 2021 00:53:55 +0200 Subject: [PATCH 18/19] Changed to termPickerProps to allow more styling --- .../docs/controls/ModernTaxonomyPicker.md | 3 +-- .../ModernTaxonomyPicker.tsx | 20 ++++++++++++------- .../TaxonomyPanelContents.tsx | 18 ++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/documentation/docs/controls/ModernTaxonomyPicker.md b/docs/documentation/docs/controls/ModernTaxonomyPicker.md index b2f10b760..187ef958c 100644 --- a/docs/documentation/docs/controls/ModernTaxonomyPicker.md +++ b/docs/documentation/docs/controls/ModernTaxonomyPicker.md @@ -68,8 +68,7 @@ The ModernTaxonomyPicker control can be configured with the following properties | placeHolder | string | no | Short text hint to display in picker. | | required | boolean | no | Specifies if to display an asterisk near the label. Default value is false. | | customPanelWidth | number | no | Custom panel width in pixels. | -| onRenderItem | function | no | Modify the display of the items in the picker. | -| onRenderSuggestionsItem | function | no | Modify the display of the items in the pickers suggestions list. | +| termPickerProps | IModernTermPickerProps | no | Custom properties for the term picker (More info: [IBasePickerProps interface](https://developer.microsoft.com/en-us/fluentui#/controls/web/pickers#IBasePickerProps)). | | themeVariant | IReadonlyTheme | no | The current loaded SharePoint theme/section background (More info: [Supporting section backgrounds](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/supporting-section-backgrounds)). | ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/TaxonomyPicker) diff --git a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx index 3799deed4..21d9123e5 100644 --- a/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx +++ b/src/controls/modernTaxonomyPicker/ModernTaxonomyPicker.tsx @@ -33,9 +33,12 @@ import { ITermInfo, } from '@pnp/sp/taxonomy'; import { TermItemSuggestion } from './termItem/TermItemSuggestion'; import { ModernTermPicker } from './modernTermPicker/ModernTermPicker'; -import { ITermItemProps } from './modernTermPicker/ModernTermPicker.types'; +import { IModernTermPickerProps, ITermItemProps } from './modernTermPicker/ModernTermPicker.types'; import { TermItem } from './termItem/TermItem'; import { IReadonlyTheme } from "@microsoft/sp-component-base"; +import { isUndefined } from 'lodash'; + +export type Optional = Pick, K> & Omit; export interface IModernTaxonomyPickerProps { allowMultipleSelections?: boolean; @@ -53,6 +56,7 @@ export interface IModernTaxonomyPickerProps { placeHolder?: string; customPanelWidth?: number; themeVariant?: IReadonlyTheme; + termPickerProps?: Optional; } export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { @@ -181,7 +185,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) { const tooltipId = useId('tooltip'); const hostStyles: Partial = { root: { display: 'inline-block' } }; const addTermButtonStyles: IButtonStyles = {rootHovered: {backgroundColor: "inherit"}, rootPressed: {backgroundColor: "inherit"}}; - const tagPickerStyles: IStyleFunctionOrObject = { input: {minheight: 34}, text: {minheight: 34} }; + const termPickerStyles: IStyleFunctionOrObject = { input: {minheight: 34}, text: {minheight: 34} }; return (
@@ -189,19 +193,20 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
{ setSelectedOptions(itms || []); setSelectedPanelOptions(itms || []); }} getTextFromItem={getTextFromItem} - pickerSuggestionsProps={{noResultsFoundText: strings.ModernTaxonomyPickerNoResultsFound}} - inputProps={{ + pickerSuggestionsProps={props.termPickerProps?.pickerSuggestionsProps ?? {noResultsFoundText: strings.ModernTaxonomyPickerNoResultsFound}} + inputProps={props.termPickerProps?.inputProps ?? { 'aria-label': props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder, placeholder: props.placeHolder || strings.ModernTaxonomyPickerDefaultPlaceHolder }} @@ -248,7 +253,7 @@ export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
) diff --git a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx index 0c4b58bf6..b4df9127b 100644 --- a/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx +++ b/src/controls/modernTaxonomyPicker/taxonomyPanelContents/TaxonomyPanelContents.tsx @@ -47,6 +47,8 @@ import * as strings from 'ControlStrings'; import { useForceUpdate } from '@uifabric/react-hooks'; import { ModernTermPicker } from '../modernTermPicker/ModernTermPicker'; import { IReadonlyTheme } from "@microsoft/sp-component-base"; +import { IModernTermPickerProps } from '../modernTermPicker/ModernTermPicker.types'; +import { Optional } from '../ModernTaxonomyPicker'; export interface ITaxonomyPanelContentsProps { context: BaseComponentContext; @@ -66,6 +68,7 @@ export interface ITaxonomyPanelContentsProps { getTextFromItem: (item: ITermInfo, currentValue?: string) => string; languageTag: string; themeVariant?: IReadonlyTheme; + termPickerProps?: Optional; } export function TaxonomyPanelContents(props: ITaxonomyPanelContentsProps): React.ReactElement { @@ -423,27 +426,28 @@ export function TaxonomyPanelContents(props: ITaxonomyPanelContentsProps): React } }; - const tagPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4, minheight: 34}, input: {minheight: 34}, text: { minheight: 34, borderStyle: 'none', borderWidth: '0px' } }; + const termPickerStyles: IStyleFunctionOrObject = { root: {paddingTop: 4, paddingBottom: 4, paddingRight: 4, minheight: 34}, input: {minheight: 34}, text: { minheight: 34, borderStyle: 'none', borderWidth: '0px' } }; return (
From 76e6011e57f90da909e8bba0e77370266e58503d Mon Sep 17 00:00:00 2001 From: Patrik Hellgren Date: Fri, 24 Sep 2021 10:53:59 +0200 Subject: [PATCH 19/19] Added additional translations --- src/loc/bg-bg.ts | 14 ++++++++++++-- src/loc/ca-es.ts | 14 ++++++++++++-- src/loc/da-dk.ts | 14 ++++++++++++-- src/loc/de-de.ts | 14 ++++++++++++-- src/loc/el-gr.ts | 14 ++++++++++++-- src/loc/en-us.ts | 6 ++++-- src/loc/es-es.ts | 14 ++++++++++++-- src/loc/et-ee.ts | 14 ++++++++++++-- src/loc/eu-es.ts | 12 +++++++++++- src/loc/fi-fi.ts | 14 ++++++++++++-- src/loc/fr-ca.ts | 14 ++++++++++++-- src/loc/fr-fr.ts | 14 ++++++++++++-- src/loc/it-it.ts | 14 ++++++++++++-- src/loc/ja-jp.ts | 14 ++++++++++++-- src/loc/lt-lt.ts | 14 ++++++++++++-- src/loc/lv-lv.ts | 14 ++++++++++++-- src/loc/mystrings.d.ts | 3 ++- src/loc/nb-no.ts | 14 ++++++++++++-- src/loc/nl-nl.ts | 16 +++++++++++++--- src/loc/pl-pl.ts | 14 ++++++++++++-- src/loc/pt-pt.ts | 14 ++++++++++++-- src/loc/ro-ro.ts | 14 ++++++++++++-- src/loc/ru-ru.ts | 14 ++++++++++++-- src/loc/sk-sk.ts | 14 ++++++++++++-- src/loc/sr-latn-rs.ts | 14 ++++++++++++-- src/loc/sv-se.ts | 4 +++- src/loc/tr-tr.ts | 14 ++++++++++++-- src/loc/vi-vn.ts | 14 ++++++++++++-- src/loc/zh-cn.ts | 14 ++++++++++++-- src/loc/zh-tw.ts | 14 ++++++++++++-- 30 files changed, 333 insertions(+), 58 deletions(-) diff --git a/src/loc/bg-bg.ts b/src/loc/bg-bg.ts index 3bc9d13fb..93d01e7f1 100644 --- a/src/loc/bg-bg.ts +++ b/src/loc/bg-bg.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Коментари", "ListItemCommentsNoCommentsLabel": "No comments", "OrgAssetsLinkLabel": "Вашата организация", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Въведете термин, който искате да маркирате", + "ModernTaxonomyPickerTreeTitle": "Изберете маркер", + "ModernTaxonomyPickerAddTagButtonTooltip": "Добавяне на маркер", + "ModernTaxonomyPickerApplyButtonText": "Използване", + "ModernTaxonomyPickerCancelButtonText": "Отказ", + "ModernTaxonomyPickerLoadMoreText": "Научете повече", + "ModernTaxonomyPickerRemoveButtonText": "Изтриване", + "ModernTaxonomyPickerPanelCloseButtonText": "Затваряне", + "ModernTaxonomyPickerNoResultsFound": "Няма намерени резултати", + "ModernTaxonomyPickerSuggestionInLabel": "Инча" }; -}); \ No newline at end of file +}); diff --git a/src/loc/ca-es.ts b/src/loc/ca-es.ts index 59b6cb42d..5e659b22f 100644 --- a/src/loc/ca-es.ts +++ b/src/loc/ca-es.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Comentaris", "ListItemCommentsNoCommentsLabel": "No comments", "OrgAssetsLinkLabel": "La vostra organització", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Introduïu un terme que vulgueu etiquetar", + "ModernTaxonomyPickerTreeTitle": "Seleccioneu una etiqueta", + "ModernTaxonomyPickerAddTagButtonTooltip": "Afegeix etiqueta", + "ModernTaxonomyPickerApplyButtonText": "Utilitza", + "ModernTaxonomyPickerCancelButtonText": "Cancel·la", + "ModernTaxonomyPickerLoadMoreText": "Més informació", + "ModernTaxonomyPickerRemoveButtonText": "Suprimeix", + "ModernTaxonomyPickerPanelCloseButtonText": "Tanca", + "ModernTaxonomyPickerNoResultsFound": "No s'han trobat resultats", + "ModernTaxonomyPickerSuggestionInLabel": "En" }; -}); \ No newline at end of file +}); diff --git a/src/loc/da-dk.ts b/src/loc/da-dk.ts index 33f18687c..24d7c0ade 100644 --- a/src/loc/da-dk.ts +++ b/src/loc/da-dk.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Kommentarer", "ListItemCommentsNoCommentsLabel": "No comments", "OrgAssetsLinkLabel": "Din organisation", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Indtast et udtryk, du vil tagge", + "ModernTaxonomyPickerTreeTitle": "Vælg et tag", + "ModernTaxonomyPickerAddTagButtonTooltip": "Tilføj tag", + "ModernTaxonomyPickerApplyButtonText": "Brug", + "ModernTaxonomyPickerCancelButtonText": "Annuller", + "ModernTaxonomyPickerLoadMoreText": "Lær mere", + "ModernTaxonomyPickerRemoveButtonText": "Slet", + "ModernTaxonomyPickerPanelCloseButtonText": "Luk", + "ModernTaxonomyPickerNoResultsFound": "Ingen resultater fundet", + "ModernTaxonomyPickerSuggestionInLabel": "i" }; -}); \ No newline at end of file +}); diff --git a/src/loc/de-de.ts b/src/loc/de-de.ts index 2bf250841..6e7a9e1df 100644 --- a/src/loc/de-de.ts +++ b/src/loc/de-de.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Kommentare", "ListItemCommentsNoCommentsLabel": "No comments", "OrgAssetsLinkLabel": "Ihre Organisation", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Geben Sie einen Begriff ein, den Sie markieren möchten", + "ModernTaxonomyPickerTreeTitle": "Wählen Sie ein Tag aus", + "ModernTaxonomyPickerAddTagButtonTooltip": "Tag hinzufügen", + "ModernTaxonomyPickerApplyButtonText": "Verwenden", + "ModernTaxonomyPickerCancelButtonText": "Abbrechen", + "ModernTaxonomyPickerLoadMoreText": "Weitere Informationen", + "ModernTaxonomyPickerRemoveButtonText": "Löschen", + "ModernTaxonomyPickerPanelCloseButtonText": "Schließen", + "ModernTaxonomyPickerNoResultsFound": "Keine Ergebnisse gefunden", + "ModernTaxonomyPickerSuggestionInLabel": "In" }; -}); \ No newline at end of file +}); diff --git a/src/loc/el-gr.ts b/src/loc/el-gr.ts index cba8cd4ee..2c1c191f8 100644 --- a/src/loc/el-gr.ts +++ b/src/loc/el-gr.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Σχόλια", "ListItemCommentsNoCommentsLabel": "No comments", "OrgAssetsLinkLabel": "Ο οργανισμός σας", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Εισαγάγετε έναν όρο που θέλετε να προσθέσετε ετικέτα", + "ModernTaxonomyPickerTreeTitle": "Επιλογή ετικέτας", + "ModernTaxonomyPickerAddTagButtonTooltip": "Προσθήκη ετικέτας", + "ModernTaxonomyPickerApplyButtonText": "Χρήση", + "ModernTaxonomyPickerCancelButtonText": "Ακύρωση", + "ModernTaxonomyPickerLoadMoreText": "Μάθετε περισσότερα", + "ModernTaxonomyPickerRemoveButtonText": "Διαγραφή", + "ModernTaxonomyPickerPanelCloseButtonText": "Κλείσιμο", + "ModernTaxonomyPickerNoResultsFound": "Δεν βρέθηκαν αποτελέσματα", + "ModernTaxonomyPickerSuggestionInLabel": "In" }; -}); \ No newline at end of file +}); diff --git a/src/loc/en-us.ts b/src/loc/en-us.ts index 566103abf..8151a97ba 100644 --- a/src/loc/en-us.ts +++ b/src/loc/en-us.ts @@ -396,6 +396,8 @@ define([], () => { ModernTaxonomyPickerCancelButtonText: "Cancel", ModernTaxonomyPickerLoadMoreText: "Load more", ModernTaxonomyPickerRemoveButtonText: "Remove", - ModernTaxonomyPickerPanelCloseButtonText: "Close" - }; + ModernTaxonomyPickerPanelCloseButtonText: "Close", + ModernTaxonomyPickerNoResultsFound: "No results found", + ModernTaxonomyPickerSuggestionInLabel: "in" +}; }); diff --git a/src/loc/es-es.ts b/src/loc/es-es.ts index 19ee9008e..2a97ef5c4 100644 --- a/src/loc/es-es.ts +++ b/src/loc/es-es.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Comentarios", "ListItemCommentsNoCommentsLabel": "No hay comentarios", "OrgAssetsLinkLabel": "Tu organizacion", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Introduzca un término que desee etiquetar", + "ModernTaxonomyPickerTreeTitle": "Seleccione una etiqueta", + "ModernTaxonomyPickerAddTagButtonTooltip": "Agregar etiqueta", + "ModernTaxonomyPickerApplyButtonText": "Usar", + "ModernTaxonomyPickerCancelButtonText": "Cancelar", + "ModernTaxonomyPickerLoadMoreText": "Más información", + "ModernTaxonomyPickerRemoveButtonText": "Eliminar", + "ModernTaxonomyPickerPanelCloseButtonText": "Cerrar", + "ModernTaxonomyPickerNoResultsFound": "No se encontraron resultados", + "ModernTaxonomyPickerSuggestionInLabel": "En" }; -}); \ No newline at end of file +}); diff --git a/src/loc/et-ee.ts b/src/loc/et-ee.ts index 0175d93f1..57ef69e6b 100644 --- a/src/loc/et-ee.ts +++ b/src/loc/et-ee.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Kommentaarid", "ListItemCommentsNoCommentsLabel": "Kommentaare pole", "OrgAssetsLinkLabel": "Teie organisatsioon", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Sisestage termin, mille soovite märgistada", + "ModernTaxonomyPickerTreeTitle": "Valige silt", + "ModernTaxonomyPickerAddTagButtonTooltip": "Lisa silt", + "ModernTaxonomyPickerApplyButtonText": "Kasuta", + "ModernTaxonomyPickerCancelButtonText": "Tühista", + "ModernTaxonomyPickerLoadMoreText": "Lisateave", + "ModernTaxonomyPickerRemoveButtonText": "Kustuta", + "ModernTaxonomyPickerPanelCloseButtonText": "Sule", + "ModernTaxonomyPickerNoResultsFound": "Tulemusi ei leitud", + "ModernTaxonomyPickerSuggestionInLabel": "Sisse" }; -}); \ No newline at end of file +}); diff --git a/src/loc/eu-es.ts b/src/loc/eu-es.ts index a0af3969c..66b4d1231 100644 --- a/src/loc/eu-es.ts +++ b/src/loc/eu-es.ts @@ -345,6 +345,16 @@ define([], () => { SelectIcon: "Hautatu ikonoa", StockImagesLinkLabel: "Stock irudiak", StockImagesHeader: "Stock irudiak", - OrgAssetsLinkLabel: "Zure erakundea" + OrgAssetsLinkLabel: "Zure erakundea", + ModernTaxonomyPickerDefaultPlaceHolder: "Idatzi etiketatu nahi duzun terminoa", + ModernTaxonomyPickerTreeTitle: "Aukeratu etiketa", + ModernTaxonomyPickerAddTagButtonTooltip: "Gehitu etiketa", + ModernTaxonomyPickerApplyButtonText: "Erabili", + ModernTaxonomyPickerCancelButtonText: "Utzi", + ModernTaxonomyPickerLoadMoreText: "Lortu informazio gehiago", + ModernTaxonomyPickerRemoveButtonText: "Ezabatu", + ModernTaxonomyPickerPanelCloseButtonText: "Itxi", + ModernTaxonomyPickerNoResultsFound: "Ez da emaitzarik aurkitu", + ModernTaxonomyPickerSuggestionInLabel: "in" }; }); diff --git a/src/loc/fi-fi.ts b/src/loc/fi-fi.ts index 5393addf1..070dbfbfb 100644 --- a/src/loc/fi-fi.ts +++ b/src/loc/fi-fi.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Kommentit", "ListItemCommentsNoCommentsLabel": "Kommentteja ei ole", "OrgAssetsLinkLabel": "Oma organisaatio", - "MyTeamsMessageDontHaveTeams": "Sinulla ei ole yhtään tiimejä" + "MyTeamsMessageDontHaveTeams": "Sinulla ei ole yhtään tiimejä", + "ModernTaxonomyPickerDefaultPlaceHolder": "Kirjoita tagi, jonka haluat merkitä", + "ModernTaxonomyPickerTreeTitle": "Valitse tunniste", + "ModernTaxonomyPickerAddTagButtonTooltip": "Lisää tunniste", + "ModernTaxonomyPickerApplyButtonText": "Käytä", + "ModernTaxonomyPickerCancelButtonText": "Peruuta", + "ModernTaxonomyPickerLoadMoreText": "Lisätietoja", + "ModernTaxonomyPickerRemoveButtonText": "Poista", + "ModernTaxonomyPickerPanelCloseButtonText": "Sulje", + "ModernTaxonomyPickerNoResultsFound": "Ei tuloksia", + "ModernTaxonomyPickerSuggestionInLabel": "Tuumaa" }; -}); \ No newline at end of file +}); diff --git a/src/loc/fr-ca.ts b/src/loc/fr-ca.ts index 8b0003ae3..07244747e 100644 --- a/src/loc/fr-ca.ts +++ b/src/loc/fr-ca.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Commentaires", "ListItemCommentsNoCommentsLabel": "Il n’y a pas de commentaires", "OrgAssetsLinkLabel": "Votre organisation", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Entrez un terme que vous souhaitez marquer", + "ModernTaxonomyPickerTreeTitle": "Sélectionnez une balise", + "ModernTaxonomyPickerAddTagButtonTooltip": "Ajouter une balise", + "ModernTaxonomyPickerApplyButtonText": "Utiliser", + "ModernTaxonomyPickerCancelButtonText": "Annuler", + "ModernTaxonomyPickerLoadMoreText": "En savoir plus", + "ModernTaxonomyPickerRemoveButtonText": "Supprimer", + "ModernTaxonomyPickerPanelCloseButtonText": "Fermer", + "ModernTaxonomyPickerNoResultsFound": "Aucun résultat trouvé", + "ModernTaxonomyPickerSuggestionInLabel": "dans" }; -}); \ No newline at end of file +}); diff --git a/src/loc/fr-fr.ts b/src/loc/fr-fr.ts index 2ef60d702..29be2107b 100644 --- a/src/loc/fr-fr.ts +++ b/src/loc/fr-fr.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Commentaires", "ListItemCommentsNoCommentsLabel": "Il n’y a pas de commentaires", "OrgAssetsLinkLabel": "Votre organisation", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Entrez un terme que vous souhaitez marquer", + "ModernTaxonomyPickerTreeTitle": "Sélectionnez une balise", + "ModernTaxonomyPickerAddTagButtonTooltip": "Ajouter une balise", + "ModernTaxonomyPickerApplyButtonText": "Utiliser", + "ModernTaxonomyPickerCancelButtonText": "Annuler", + "ModernTaxonomyPickerLoadMoreText": "En savoir plus", + "ModernTaxonomyPickerRemoveButtonText": "Supprimer", + "ModernTaxonomyPickerPanelCloseButtonText": "Fermer", + "ModernTaxonomyPickerNoResultsFound": "Aucun résultat trouvé", + "ModernTaxonomyPickerSuggestionInLabel": "dans" }; -}); \ No newline at end of file +}); diff --git a/src/loc/it-it.ts b/src/loc/it-it.ts index 84f8b8d6e..bc1c9ae6b 100644 --- a/src/loc/it-it.ts +++ b/src/loc/it-it.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Commenti", "ListItemCommentsNoCommentsLabel": "Non ci sono commenti", "OrgAssetsLinkLabel": "Tua organizzazione", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Inserisci un termine che vuoi taggare", + "ModernTaxonomyPickerTreeTitle": "Seleziona un tag", + "ModernTaxonomyPickerAddTagButtonTooltip": "Aggiungi tag", + "ModernTaxonomyPickerApplyButtonText": "Usa", + "ModernTaxonomyPickerCancelButtonText": "Annulla", + "ModernTaxonomyPickerLoadMoreText": "Ulteriori informazioni", + "ModernTaxonomyPickerRemoveButtonText": "Elimina", + "ModernTaxonomyPickerPanelCloseButtonText": "Chiudi", + "ModernTaxonomyPickerNoResultsFound": "Nessun risultato trovato", + "ModernTaxonomyPickerSuggestionInLabel": "Pollici" }; -}); \ No newline at end of file +}); diff --git a/src/loc/ja-jp.ts b/src/loc/ja-jp.ts index 222f6f8b9..1522a9874 100644 --- a/src/loc/ja-jp.ts +++ b/src/loc/ja-jp.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "コメント", "ListItemCommentsNoCommentsLabel": "コメントはありません", "OrgAssetsLinkLabel": "あなたの組織", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "タグ付けする用語を入力してください", + "ModernTaxonomyPickerTreeTitle": "タグを選択", + "ModernTaxonomyPickerAddTagButtonTooltip": "タグを追加", + "ModernTaxonomyPickerApplyButtonText": "使用", + "ModernTaxonomyPickerCancelButtonText": "キャンセル", + "ModernTaxonomyPickerLoadMoreText": "詳細", + "ModernTaxonomyPickerRemoveButtonText": "削除", + "ModernTaxonomyPickerPanelCloseButtonText": "閉じる", + "ModernTaxonomyPickerNoResultsFound": "結果が見つかりませんでした", + "ModernTaxonomyPickerSuggestionInLabel": "インチ" }; -}); \ No newline at end of file +}); diff --git a/src/loc/lt-lt.ts b/src/loc/lt-lt.ts index 377ad4401..7dda3ab84 100644 --- a/src/loc/lt-lt.ts +++ b/src/loc/lt-lt.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Komentarai", "ListItemCommentsNoCommentsLabel": "Komentarų nėra", "OrgAssetsLinkLabel": "Savo organizacijoje", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Įveskite terminą, kurį norite pažymėti", + "ModernTaxonomyPickerTreeTitle": "Pasirinkite žymą", + "ModernTaxonomyPickerAddTagButtonTooltip": "Pridėti žymą", + "ModernTaxonomyPickerApplyButtonText": "Naudoti", + "ModernTaxonomyPickerCancelButtonText": "Atšaukti", + "ModernTaxonomyPickerLoadMoreText": "Sužinokite daugiau", + "ModernTaxonomyPickerRemoveButtonText": "Ištrinti", + "ModernTaxonomyPickerPanelCloseButtonText": "Uždaryti", + "ModernTaxonomyPickerNoResultsFound": "Nerasta rezultatų", + "ModernTaxonomyPickerSuggestionInLabel": "" }; -}); \ No newline at end of file +}); diff --git a/src/loc/lv-lv.ts b/src/loc/lv-lv.ts index 0a085f593..3c1abfc3b 100644 --- a/src/loc/lv-lv.ts +++ b/src/loc/lv-lv.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Komentāri", "ListItemCommentsNoCommentsLabel": "Nav komentāru", "OrgAssetsLinkLabel": "Jūsu organizācija", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Ievadiet vārdu, kuru vēlaties atzīmēt", + "ModernTaxonomyPickerTreeTitle": "Atlasiet tagu", + "ModernTaxonomyPickerAddTagButtonTooltip": "Pievienot tagu", + "ModernTaxonomyPickerApplyButtonText": "Izmantot", + "ModernTaxonomyPickerCancelButtonText": "Atcelt", + "ModernTaxonomyPickerLoadMoreText": "Uzziniet vairāk", + "ModernTaxonomyPickerRemoveButtonText": "Dzēst", + "ModernTaxonomyPickerPanelCloseButtonText": "Aizvērt", + "ModernTaxonomyPickerNoResultsFound": "Rezultāti nav atrasti", + "ModernTaxonomyPickerSuggestionInLabel": "Collas" }; -}); \ No newline at end of file +}); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 5cd933c1a..4383b1a72 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -6,7 +6,6 @@ declare interface IControlStrings { MyTeamsMessageError:string; MyTeamsNoTeamsMessage:string; - MyTeamsLoadingMessage:string; MyTeamsLoadingMessage: string; MyTeamsTeamChannelPublicMessage: string; MyTeamsTeamChannelTypeMessage: string; @@ -373,6 +372,8 @@ declare interface IControlStrings { ModernTaxonomyPickerLoadMoreText: string; ModernTaxonomyPickerRemoveButtonText: string; ModernTaxonomyPickerPanelCloseButtonText: string; + ModernTaxonomyPickerNoResultsFound: string; + ModernTaxonomyPickerSuggestionInLabel: string; } declare interface IDateTimeStrings { diff --git a/src/loc/nb-no.ts b/src/loc/nb-no.ts index 6d2a56ee0..447d6510d 100644 --- a/src/loc/nb-no.ts +++ b/src/loc/nb-no.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Kommentarer", "ListItemCommentsNoCommentsLabel": "Det finnes ingen kommentarer", "OrgAssetsLinkLabel": "Egen organisasjon", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Skriv inn termen som skal merkes", + "ModernTaxonomyPickerTreeTitle": "Velg ett eller flere merker", + "ModernTaxonomyPickerAddTagButtonTooltip": "Legg til merke", + "ModernTaxonomyPickerApplyButtonText": "Bruk", + "ModernTaxonomyPickerCancelButtonText": "Avbryt", + "ModernTaxonomyPickerLoadMoreText": "Les inn mer", + "ModernTaxonomyPickerRemoveButtonText": "Fjerne", + "ModernTaxonomyPickerPanelCloseButtonText": "Lukk", + "ModernTaxonomyPickerNoResultsFound": "Ingen resultater funnet", + "ModernTaxonomyPickerSuggestionInLabel": "i" }; -}); \ No newline at end of file +}); diff --git a/src/loc/nl-nl.ts b/src/loc/nl-nl.ts index cfcc82686..6c7e78072 100644 --- a/src/loc/nl-nl.ts +++ b/src/loc/nl-nl.ts @@ -41,7 +41,7 @@ define([], () => { "TaxonomyPickerNoTerms": "Termen set heeft geen termen beschikbaar", "TaxonomyPickerExpandTitle": "Vouw de termen set uit", "TaxonomyPickerMenuTermSet": "Menu van de termen set", - "TaxonomyPickerInLabel": "In", + "TaxonomyPickerInLabel": "Inch", "TaxonomyPickerTermSetLabel": "Termen set", "TaxonomyPickerTermsNotFound": "De volgende geselecteerde term(en) is(en) niet gevonden in het termenarchief: {0}", "TaxonomyPickerInvalidTerms": "Ongeldige term(en) repareren: {0}", @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Opmerkingen", "ListItemCommentsNoCommentsLabel": "Er is geen commentaar", "OrgAssetsLinkLabel": "Uw organisatie", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Voer een term in die u wilt taggen", + "ModernTaxonomyPickerTreeTitle": "Selecteer een tag", + "ModernTaxonomyPickerAddTagButtonTooltip": "Tag toevoegen", + "ModernTaxonomyPickerApplyButtonText": "Gebruik", + "ModernTaxonomyPickerCancelButtonText": "Annuleren", + "ModernTaxonomyPickerLoadMoreText": "Meer informatie", + "ModernTaxonomyPickerRemoveButtonText": "Verwijderen", + "ModernTaxonomyPickerPanelCloseButtonText": "Sluiten", + "ModernTaxonomyPickerNoResultsFound": "Geen resultaten gevonden", + "ModernTaxonomyPickerSuggestionInLabel": "Inch" }; -}); \ No newline at end of file +}); diff --git a/src/loc/pl-pl.ts b/src/loc/pl-pl.ts index c0c61e825..91f86f963 100644 --- a/src/loc/pl-pl.ts +++ b/src/loc/pl-pl.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Komentarze", "ListItemCommentsNoCommentsLabel": "Brak komentarzy", "OrgAssetsLinkLabel": "Twoja organizacja", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Wprowadź termin, który chcesz oznaczyć", + "ModernTaxonomyPickerTreeTitle": "Wybierz tag", + "ModernTaxonomyPickerAddTagButtonTooltip": "Dodaj tag", + "ModernTaxonomyPickerApplyButtonText": "Użyj", + "ModernTaxonomyPickerCancelButtonText": "Anuluj", + "ModernTaxonomyPickerLoadMoreText": "Dowiedz się więcej", + "ModernTaxonomyPickerRemoveButtonText": "Usuń", + "ModernTaxonomyPickerPanelCloseButtonText": "Zamknij", + "ModernTaxonomyPickerNoResultsFound": "Nie znaleziono wyników", + "ModernTaxonomyPickerSuggestionInLabel": "Cala" }; -}); \ No newline at end of file +}); diff --git a/src/loc/pt-pt.ts b/src/loc/pt-pt.ts index 344664928..fdd514825 100644 --- a/src/loc/pt-pt.ts +++ b/src/loc/pt-pt.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Comentários", "ListItemCommentsNoCommentsLabel": "Sem Comentários", "OrgAssetsLinkLabel": "Sua organização", - "MyTeamsMessageDontHaveTeams": "Neste momento não tens nenhum Team" + "MyTeamsMessageDontHaveTeams": "Neste momento não tens nenhum Team", + "ModernTaxonomyPickerDefaultPlaceHolder": "Digite um termo que deseja marcar", + "ModernTaxonomyPickerTreeTitle": "Selecione uma tag", + "ModernTaxonomyPickerAddTagButtonTooltip": "Adicionar tag", + "ModernTaxonomyPickerApplyButtonText": "Use", + "ModernTaxonomyPickerCancelButtonText": "Cancelar", + "ModernTaxonomyPickerLoadMoreText": "Saiba mais", + "ModernTaxonomyPickerRemoveButtonText": "Delete", + "ModernTaxonomyPickerPanelCloseButtonText": "Fechar", + "ModernTaxonomyPickerNoResultsFound": "Nenhum resultado encontrado", + "ModernTaxonomyPickerSuggestionInLabel": "Em" }; -}); \ No newline at end of file +}); diff --git a/src/loc/ro-ro.ts b/src/loc/ro-ro.ts index b88b1815a..834d6eeeb 100644 --- a/src/loc/ro-ro.ts +++ b/src/loc/ro-ro.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Comentarii", "ListItemCommentsNoCommentsLabel": "Nu există comentarii", "OrgAssetsLinkLabel": "Organizația dvs.", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Introduceți un termen pe care doriți să-l etichetați", + "ModernTaxonomyPickerTreeTitle": "Selectați o etichetă", + "ModernTaxonomyPickerAddTagButtonTooltip": "Adaugă etichetă", + "ModernTaxonomyPickerApplyButtonText": "Utilizați", + "ModernTaxonomyPickerCancelButtonText": "Anulați", + "ModernTaxonomyPickerLoadMoreText": "Aflați mai multe", + "ModernTaxonomyPickerRemoveButtonText": "Ștergeți", + "ModernTaxonomyPickerPanelCloseButtonText": "Închide", + "ModernTaxonomyPickerNoResultsFound": "Nu s-au găsit rezultate", + "ModernTaxonomyPickerSuggestionInLabel": "In" }; -}); \ No newline at end of file +}); diff --git a/src/loc/ru-ru.ts b/src/loc/ru-ru.ts index ce3dba043..53a1732d1 100644 --- a/src/loc/ru-ru.ts +++ b/src/loc/ru-ru.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Комментарии", "ListItemCommentsNoCommentsLabel": "Нет комментариев", "OrgAssetsLinkLabel": "Ваша организация", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Введите термин, который нужно пометить", + "ModernTaxonomyPickerTreeTitle": "Выберите тег", + "ModernTaxonomyPickerAddTagButtonTooltip": "Добавить тег", + "ModernTaxonomyPickerApplyButtonText": "Использование", + "ModernTaxonomyPickerCancelButtonText": "Отмена", + "ModernTaxonomyPickerLoadMoreText": "Подробнее", + "ModernTaxonomyPickerRemoveButtonText": "Удалить", + "ModernTaxonomyPickerPanelCloseButtonText": "Закрыть", + "ModernTaxonomyPickerNoResultsFound": "Ничего не найдено", + "ModernTaxonomyPickerSuggestionInLabel": "в" }; -}); \ No newline at end of file +}); diff --git a/src/loc/sk-sk.ts b/src/loc/sk-sk.ts index 1decde573..319213e02 100644 --- a/src/loc/sk-sk.ts +++ b/src/loc/sk-sk.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Komentáre", "ListItemCommentsNoCommentsLabel": "Nie sú žiadne komentáre", "OrgAssetsLinkLabel": "Vašej organizácie", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Zadajte výraz, ktorý chcete označiť", + "ModernTaxonomyPickerTreeTitle": "Vyberte značku", + "ModernTaxonomyPickerAddTagButtonTooltip": "Pridať značku", + "ModernTaxonomyPickerApplyButtonText": "Použiť", + "ModernTaxonomyPickerCancelButtonText": "Zrušiť", + "ModernTaxonomyPickerLoadMoreText": "Viac informácií", + "ModernTaxonomyPickerRemoveButtonText": "Odstrániť", + "ModernTaxonomyPickerPanelCloseButtonText": "Zavrieť", + "ModernTaxonomyPickerNoResultsFound": "Nenašli sa žiadne výsledky", + "ModernTaxonomyPickerSuggestionInLabel": "Palcov" }; -}); \ No newline at end of file +}); diff --git a/src/loc/sr-latn-rs.ts b/src/loc/sr-latn-rs.ts index bf4a616cb..040364126 100644 --- a/src/loc/sr-latn-rs.ts +++ b/src/loc/sr-latn-rs.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Komentare", "ListItemCommentsNoCommentsLabel": "Nema komentara", "OrgAssetsLinkLabel": "Vaša organizacija", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTakonomiPickerDefaultPlaceHolder": "Unesite termin koji želite da označite", + "ModernTakonomiPickerTreeTitle": "Izaberite oznaku", + "ModernTakonomiPickerAddTagButtonTooltip": "Dodaj oznaku", + "ModernTakonomiPickerAppliButtonTekt": "Koristi", + "ModernTakonomiPickerCancelButtonTekt": "Otkaži", + "ModernTakonomiPickerLoadMoreTekt": "Saznajte više", + "ModernTakonomiPickerRemoveButtonTekt": "Izbriši", + "ModernTakonomiPickerPanelCloseButtonTekt": "Zatvori", + "ModernTaxonomyPickerNoResultsFound": "Nisu pronađeni rezultati", + "ModernTaxonomyPickerSuggestionInLabel": "Inča" }; -}); \ No newline at end of file +}); diff --git a/src/loc/sv-se.ts b/src/loc/sv-se.ts index 2a2cb4ae4..103df4e16 100644 --- a/src/loc/sv-se.ts +++ b/src/loc/sv-se.ts @@ -376,6 +376,8 @@ define([], () => { "ModernTaxonomyPickerCancelButtonText": "Avbryt", "ModernTaxonomyPickerLoadMoreText": "Läs in mer", "ModernTaxonomyPickerRemoveButtonText": "Ta bort", - "ModernTaxonomyPickerPanelCloseButtonText": "Stäng" + "ModernTaxonomyPickerPanelCloseButtonText": "Stäng", + "ModernTaxonomyPickerNoResultsFound": "Inga resultat hittades", + "ModernTaxonomyPickerSuggestionInLabel": "i" }; }); diff --git a/src/loc/tr-tr.ts b/src/loc/tr-tr.ts index 8f734180c..ed77ef4f7 100644 --- a/src/loc/tr-tr.ts +++ b/src/loc/tr-tr.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Yorum", "ListItemCommentsNoCommentsLabel": "Yorum Yok", "OrgAssetsLinkLabel": "Kuruluşunuz", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Etiketlemek istediğiniz bir terim girin", + "ModernTaxonomyPickerTreeTitle": "Bir etiket seçin", + "ModernTaxonomyPickerAddTagButtonTooltip": "Etiket Ekle", + "ModernTaxonomyPickerApplyButtonText": "Kullan", + "ModernTaxonomyPickerCancelButtonText": "İptal", + "ModernTaxonomyPickerLoadMoreText": "Daha fazla bilgi edinin", + "ModernTaxonomyPickerRemoveButtonText": "Sil", + "ModernTaxonomyPickerPanelCloseButtonText": "Kapat", + "ModernTaxonomyPickerNoResultsFound": "Sonuç bulunamadı", + "ModernTaxonomyPickerSuggestionInLabel": "Inç" }; -}); \ No newline at end of file +}); diff --git a/src/loc/vi-vn.ts b/src/loc/vi-vn.ts index ae02b379d..30ae2b989 100644 --- a/src/loc/vi-vn.ts +++ b/src/loc/vi-vn.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "Ý kiến", "ListItemCommentsNoCommentsLabel": "Không có bình luận", "OrgAssetsLinkLabel": "Tổ chức của bạn", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "Nhập cụm từ bạn muốn gắn thẻ", + "ModernTaxonomyPickerTreeTitle": "Chọn thẻ", + "ModernTaxonomyPickerAddTagButtonTooltip": "Thêm thẻ", + "ModernTaxonomyPickerApplyButtonText": "Sử dụng", + "ModernTaxonomyPickerCancelButtonText": "Hủy", + "ModernTaxonomyPickerLoadMoreText": "Tìm hiểu thêm", + "ModernTaxonomyPickerRemoveButtonText": "Xóa", + "ModernTaxonomyPickerPanelCloseButtonText": "Đóng", + "ModernTaxonomyPickerNoResultsFound": "Không tìm thấy kết quả", + "ModernTaxonomyPickerSuggestionInLabel": "In" }; -}); \ No newline at end of file +}); diff --git a/src/loc/zh-cn.ts b/src/loc/zh-cn.ts index 4b3443810..773308437 100644 --- a/src/loc/zh-cn.ts +++ b/src/loc/zh-cn.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "评论", "ListItemCommentsNoCommentsLabel": "没有评论", "OrgAssetsLinkLabel": "您的组织", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "输入您要标记的术语", + "ModernTaxonomyPickerTreeTitle": "选择一个标签", + "ModernTaxonomyPickerAddTagButtonTooltip": "添加标签", + "ModernTaxonomyPickerApplyButtonText": "使用", + "ModernTaxonomyPickerCancelButtonText": "取消", + "ModernTaxonomyPickerLoadMoreText": "了解更多", + "ModernTaxonomyPickerRemoveButtonText": "删除", + "ModernTaxonomyPickerPanelCloseButtonText": "关闭", + "ModernTaxonomyPickerNoResultsFound": "未找到结果", + "ModernTaxonomyPickerSuggestionInLabel": "在" }; -}); \ No newline at end of file +}); diff --git a/src/loc/zh-tw.ts b/src/loc/zh-tw.ts index b87a4bbed..9a063b379 100644 --- a/src/loc/zh-tw.ts +++ b/src/loc/zh-tw.ts @@ -368,6 +368,16 @@ define([], () => { "ListItemCommentsLabel": "評論", "ListItemCommentsNoCommentsLabel": "沒有評論", "OrgAssetsLinkLabel": "您的組織", - "MyTeamsMessageDontHaveTeams": "You don't have any teams" + "MyTeamsMessageDontHaveTeams": "You don't have any teams", + "ModernTaxonomyPickerDefaultPlaceHolder": "輸入您要標記的術語", + "ModernTaxonomyPickerTreeTitle": "選擇一個標籤", + "ModernTaxonomyPickerAddTagButtonTooltip": "添加標籤", + "ModernTaxonomyPickerApplyButtonText": "使用", + "ModernTaxonomyPickerCancelButtonText": "取消", + "ModernTaxonomyPickerLoadMoreText": "了解更多", + "ModernTaxonomyPickerRemoveButtonText": "刪除", + "ModernTaxonomyPickerPanelCloseButtonText": "關閉", + "ModernTaxonomyPickerNoResultsFound": "未找到結果", + "ModernTaxonomyPickerSuggestionInLabel": "在" }; -}); \ No newline at end of file +});