Skip to content

Commit 49603ac

Browse files
Merge pull request #1527 from pnp/NavigationControl
new control TermSetNavigation
2 parents c03b580 + 1888d0d commit 49603ac

59 files changed

Lines changed: 1200 additions & 29 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
886 KB
Loading
268 KB
Loading
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# TermSetNavigation
2+
3+
This control allows you to navigate and select a Term from a TermSet. You can also configure a context menu for a term to execute a specific action.
4+
5+
6+
7+
**TermSetNavigation**
8+
9+
![termsetNavigation](../assets/TermSetNavigation.png)
10+
11+
![termsetNavigation](../assets/TermSetNavigation.gif)
12+
13+
14+
15+
## How to use this control in your solutions
16+
17+
- 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.
18+
- Import the following modules to your component:
19+
20+
```TypeScript
21+
import { TermSetNavigation } from '@pnp/spfx-controls-react/lib/TermSetNavigation';
22+
```
23+
24+
- Use the `TermSetNavigation` control in your code as follows:
25+
26+
```TypeScript
27+
<TermSetNavigation
28+
context={context}
29+
themeVariant={themeVariant}
30+
termSetId={"289180a0-4a8b-4f08-ae6e-ea3fb1b669e2"}
31+
showContextMenu={true}
32+
contextMenuItems={[
33+
{
34+
key: "add",
35+
text: "Add",
36+
iconProps: { iconName: "add" },
37+
},
38+
{
39+
key: "adit",
40+
text: "Edit",
41+
iconProps: { iconName: "Edit" },
42+
},
43+
{
44+
key: "remove",
45+
text: "Remove",
46+
iconProps: { iconName: "delete" },
47+
},
48+
]}
49+
onSelected={onSelect}
50+
onSelectedTermAction={onSelectedTermAction}
51+
/>
52+
```
53+
54+
- With the `onSelected` property you can get the selcted term:
55+
56+
```typescript
57+
const onSelect = React.useCallback((selected: TermStore.Term) => {
58+
console.log(selected);
59+
}, []);
60+
61+
```
62+
63+
- With the `onSelectedTermAction` property you can get the the action on the contextMenu for tghe selcted term:
64+
65+
```typescript
66+
const onSelectedTermAction = React.useCallback((selected: TermStore.Term, option:string) => {
67+
console.log(selected, option);
68+
}, []);
69+
70+
```
71+
72+
73+
## Implementation
74+
75+
The TermSetNavigation control can be configured with the following properties:
76+
77+
| Property | Type | Required | Description |
78+
| ---- | ---- | ---- | ---- |
79+
| themeVariant | IReadonlyTheme | yes | ThemeVariant |
80+
| termSetId | string | yes | Term Set Id |
81+
| context | BaseComponentContext | yes | Context of the current web part or extension. |
82+
| showContextMenu | boolean | no | If show ConextMenu for term |
83+
| contextMenuItems |IContextualMenuItem[] | no | array of action to show on contextMenu, if is un defined the conbtecxtMenu won't be available |
84+
| onSelected |onSelected?: (term: TermStore.Term) => void| no | return Term Sselcted |
85+
| onSelectedTermAction | onSelectedTermAction?: (term : TermStore.Term, option:string) => void |no | return the action selected to to term |
86+
87+
## MSGraph Permissions required
88+
89+
This control required the flowing scopes :
90+
91+
at least : , TermStore.Read.All, TermStore.ReadWrite.All,
92+
93+
please use M365Cli or PnP Powershell to add these permissions.
94+
95+
![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/TermSetNavigation)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"dependencies": {
2323
"@fluentui/react-file-type-icons": "^8.8.3",
2424
"@fluentui/react-hooks": "^8.2.6",
25+
"@fluentui/react-icons": "^2.0.200",
2526
"@fluentui/react-northstar": "0.66.0",
2627
"@fluentui/react-theme-provider": "^0.19.16",
2728
"@fluentui/scheme-utilities": "^8.2.12",

src/TermSetNavigation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './controls/TermSetNavigation/index';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from 'react';
2+
3+
import {
4+
MessageBar,
5+
MessageBarType,
6+
} from '@fluentui/react/lib/MessageBar';
7+
import { Stack } from '@fluentui/react/lib/Stack';
8+
9+
import { IErrorMessageProps } from '../models/IErrorMessageProps';
10+
import { useErrorMessageStyles } from './useErrorMessageStyles';
11+
12+
export const ErrorMessage: React.FunctionComponent<IErrorMessageProps> = (
13+
props: React.PropsWithChildren<IErrorMessageProps>
14+
) => {
15+
const { showError, errorMessage, children } = props;
16+
const { messageErrorContainerStyles} = useErrorMessageStyles();
17+
if (!showError) return null;
18+
return (
19+
<>
20+
<Stack verticalAlign="center" tokens={{ childrenGap: 10 }} styles={messageErrorContainerStyles}>
21+
<MessageBar messageBarType={MessageBarType.error} isMultiline={true}>
22+
{errorMessage}
23+
{children}
24+
</MessageBar>
25+
</Stack>
26+
</>
27+
);
28+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
3+
import { useAtom } from 'jotai';
4+
5+
import { IStackStyles } from '@fluentui/react/lib/Stack';
6+
7+
import { globalState } from '../atoms/globalState';
8+
9+
interface IErrorMessageStyles {
10+
messageErrorContainerStyles: IStackStyles;
11+
}
12+
13+
export const useErrorMessageStyles = ():IErrorMessageStyles => {
14+
const [appGlobalState] = useAtom(globalState);
15+
const { themeVariant } = appGlobalState;
16+
const messageErrorContainerStyles: IStackStyles = React.useMemo(() => {
17+
return {
18+
root: {
19+
width:'100%',
20+
},
21+
};
22+
}, [ themeVariant]);
23+
24+
return {messageErrorContainerStyles}
25+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { IContextualMenuItem } from '@fluentui/react';
2+
import { ITheme } from '@fluentui/react/lib/Styling';
3+
import { TermStore } from '@microsoft/microsoft-graph-types';
4+
import {
5+
BaseComponentContext,
6+
IReadonlyTheme,
7+
} from '@microsoft/sp-component-base';
8+
9+
export interface INavigationProps {
10+
context: BaseComponentContext;
11+
themeVariant:IReadonlyTheme | ITheme;
12+
termSetId: string;
13+
showContextMenu: boolean;
14+
contextMenuItems?: IContextualMenuItem[];
15+
onSelected?: (term: TermStore.Term) => void;
16+
onSelectedTermAction?: (term : TermStore.Term, option:string) => void;
17+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/* eslint-disable require-atomic-updates */
2+
/* eslint-disable @typescript-eslint/no-floating-promises */
3+
import * as React from 'react';
4+
5+
import * as strings from 'ControlStrings';
6+
import { useAtom } from 'jotai';
7+
8+
import {
9+
INav,
10+
INavLink,
11+
INavLinkGroup,
12+
Nav,
13+
} from '@fluentui/react/lib/Nav';
14+
import { Separator } from '@fluentui/react/lib/Separator';
15+
import { Spinner } from '@fluentui/react/lib/Spinner';
16+
import { Stack } from '@fluentui/react/lib/Stack';
17+
import { Text } from '@fluentui/react/lib/text';
18+
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
19+
import { TermStore } from '@microsoft/microsoft-graph-types';
20+
21+
import { globalState } from './atoms/globalState';
22+
import { ErrorMessage } from './ErrorMessage/ErrorMessage';
23+
import { useGraphTaxonomyAPI } from './hooks/useGraphTaxonomyAPI';
24+
import { useSessionStorage } from './hooks/useSessionStorage';
25+
import { INavigationProps } from './INavigationProps';
26+
import { RenderLink } from './RenderLink';
27+
import { RenderNoOptions } from './RenderNoOptions';
28+
import { useNavigationStyles } from './useNavigationStyles';
29+
import { useTaxonomyUtils } from './utils/useTaxonomyUtils';
30+
import { useUtils } from './utils/useUtils';
31+
32+
export const Navigation: React.FunctionComponent<INavigationProps> = (
33+
props: React.PropsWithChildren<INavigationProps>
34+
) => {
35+
const { context, termSetId } = props;
36+
const [appGlobalState, setAppGlobalState] = useAtom(globalState);
37+
const { isLoadingNavitionTree, refreshNavigationTree, showContextMenu, onSelected } = appGlobalState || {};
38+
const { navStyles } = useNavigationStyles();
39+
const { pageContext } = context || {};
40+
const { site } = pageContext || {};
41+
const { getTermSetChildren, getTermSet } = useGraphTaxonomyAPI(context);
42+
const isLoadedTermSetsRef = React.useRef<boolean>(false);
43+
const { createItems } = useTaxonomyUtils(context);
44+
const [navLinksGroup, setNavLinksGroup] = React.useState<INavLinkGroup[]>([]);
45+
const { selectedItem } = appGlobalState;
46+
const navRef = React.useRef<INav>(null);
47+
const { getCacheKey } = useUtils();
48+
49+
const [getSessionStorageItem] = useSessionStorage();
50+
const [termSetInfo, setTermSetInfo] = React.useState<TermStore.Set>(null);
51+
const [error, setError] = React.useState<Error>(null);
52+
53+
const loadNavLinks = React.useCallback(
54+
async (refresh: boolean) => {
55+
if (!site) return;
56+
57+
setError(null);
58+
setAppGlobalState((state) => ({ ...state, isLoadingNavitionTree: true }));
59+
isLoadedTermSetsRef.current = true;
60+
const termSetChildren = await getTermSetChildren(site?.id.toString(), termSetId, refresh);
61+
const navItems = await createItems(site?.id.toString(), termSetId, termSetChildren, 0, refresh);
62+
const navGroup = { groupData: { termSet: termSetId }, links: navItems };
63+
setNavLinksGroup((state) => [...state, navGroup]);
64+
setAppGlobalState((state) => ({
65+
...state,
66+
selectedItem: selectedItem ?? navItems[0],
67+
isLoadingNavitionTree: false,
68+
refreshNavigationTree: false,
69+
}));
70+
},
71+
[
72+
selectedItem,
73+
site,
74+
getSessionStorageItem,
75+
getCacheKey,
76+
getTermSetChildren,
77+
createItems,
78+
setAppGlobalState,
79+
termSetId,
80+
]
81+
);
82+
83+
React.useEffect(() => {
84+
(async () => {
85+
try {
86+
if (!isLoadedTermSetsRef.current) {
87+
const termSetInfo = await getTermSet(site?.id.toString(), termSetId);
88+
setTermSetInfo(termSetInfo);
89+
await loadNavLinks(false);
90+
isLoadedTermSetsRef.current = true;
91+
}
92+
} catch (error) {
93+
setError(error);
94+
} finally {
95+
setAppGlobalState((state) => ({ ...state, isLoadingNavitionTree: false }));
96+
}
97+
})();
98+
}, []);
99+
100+
React.useEffect(() => {
101+
(async () => {
102+
if (refreshNavigationTree) {
103+
setNavLinksGroup([]);
104+
await loadNavLinks(refreshNavigationTree);
105+
}
106+
})();
107+
}, [refreshNavigationTree]);
108+
109+
const onRenderLink: IRenderFunction<INavLink> = React.useCallback(
110+
(link: INavLink): JSX.Element => {
111+
return <RenderLink link={link} showContextMenu={showContextMenu} />;
112+
},
113+
[showContextMenu]
114+
);
115+
116+
const onLinkClick = React.useCallback(
117+
(ev: React.MouseEvent<HTMLElement, MouseEvent>, item: INavLink) => {
118+
ev.preventDefault();
119+
setAppGlobalState((state) => ({ ...state, selectedItem: item }));
120+
if (onSelected) {
121+
onSelected(item.data);
122+
}
123+
},
124+
[selectedItem, setAppGlobalState]
125+
);
126+
127+
const hasLinks = React.useMemo(() => {
128+
return navLinksGroup[0]?.links?.length > 0;
129+
}, [navLinksGroup]);
130+
131+
const hasError = React.useMemo(() => {
132+
return error !== null || termSetInfo === null;
133+
}, [error, termSetInfo]);
134+
135+
if (isLoadingNavitionTree) return <Spinner ariaLive="assertive" />;
136+
137+
if (hasError)
138+
return (
139+
<ErrorMessage showError={hasError} errorMessage={error?.message ?? strings.TermSertNaviagtionErrorMessage} />
140+
);
141+
142+
return (
143+
<>
144+
<Stack horizontalAlign="stretch" tokens={{ childrenGap: 0 }}>
145+
<Text variant="large">{termSetInfo?.localizedNames[0].name ?? ""}</Text>
146+
<Separator />
147+
{hasLinks ? (
148+
<Nav
149+
componentRef={navRef}
150+
ariaLabel="navigation"
151+
styles={navStyles}
152+
groups={navLinksGroup}
153+
onRenderLink={onRenderLink}
154+
selectedKey={selectedItem?.key}
155+
onLinkClick={onLinkClick}
156+
/>
157+
) : (
158+
<RenderNoOptions />
159+
)}
160+
</Stack>
161+
</>
162+
);
163+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as React from 'react';
2+
3+
import { useAtom } from 'jotai';
4+
5+
import { IconButton } from '@fluentui/react/lib/Button';
6+
import { IContextualMenuProps } from '@fluentui/react/lib/ContextualMenu';
7+
import { IIconProps } from '@fluentui/react/lib/Icon';
8+
9+
import { globalState } from './atoms/globalState';
10+
11+
export interface INavigationContextMenuProps {}
12+
13+
export const NavigationContextMenu: React.FunctionComponent<INavigationContextMenuProps> = (
14+
props: React.PropsWithChildren<INavigationContextMenuProps>
15+
) => {
16+
const moreVerticaliIcon: IIconProps = { iconName: "MoreVertical" };
17+
const [appGlobalState] = useAtom(globalState);
18+
const { contextMenuItems, onSelectedTermAction, selectedItem } = appGlobalState;
19+
const menuProps: IContextualMenuProps = React.useMemo(() => {
20+
return {
21+
items: contextMenuItems.length ? contextMenuItems : [],
22+
directionalHintFixed: true,
23+
onItemClick: (ev, item) => {
24+
ev.preventDefault();
25+
onSelectedTermAction(selectedItem.data, item.text);
26+
},
27+
};
28+
}, [contextMenuItems]);
29+
30+
return (
31+
<>
32+
<IconButton
33+
menuProps={menuProps}
34+
iconProps={moreVerticaliIcon}
35+
ariaLabel="Menu"
36+
styles={{ menuIcon: { display: "none" } }}
37+
/>
38+
</>
39+
);
40+
};

0 commit comments

Comments
 (0)