src/admin/navigation/navigation.tsx (259 lines of code) (raw):
import * as React from 'react';
import { EventManager } from '@paperbits/common/events';
import { INavigationService } from '@paperbits/common/navigation';
import { NavigationItemContract } from '@paperbits/common/navigation/navigationItemContract';
import { Resolve } from '@paperbits/react/decorators';
import { CommandBarButton, FontIcon, IIconProps, INavLink, INavLinkGroup, Nav, Spinner, Stack, Text } from '@fluentui/react';
import { BackButton } from '../utils/components/backButton';
import { DeleteConfirmationOverlay } from '../utils/components/deleteConfirmationOverlay';
import { NavigationItemModal } from './navigationItemModal';
interface NavigationState {
navigationItems: NavigationItemContract[],
navigationItemsToRender: INavLinkGroup[], // needed as Nav component uses different structure
hoveredNavItem: string,
currentNavItem: NavigationItemContract,
showDeleteConfirmation: boolean,
showNavigationItemModal: boolean,
isLoading: boolean
}
interface PagesProps {
onBackButtonClick: () => void
}
const enum ItemPosition {
FIRST,
LAST,
FIRST_AND_LAST,
NOT_FIRST_OR_LAST
}
const addIcon: IIconProps = { iconName: 'Add' };
const iconStyles = { width: '16px' };
export class Navigation extends React.Component<PagesProps, NavigationState> {
@Resolve('navigationService')
public navigationService: INavigationService;
@Resolve('eventManager')
public eventManager: EventManager;
constructor(props: PagesProps) {
super(props);
this.state = {
navigationItems: [],
navigationItemsToRender: [],
hoveredNavItem: null,
currentNavItem: null,
showDeleteConfirmation: false,
showNavigationItemModal: false,
isLoading: false
}
}
componentDidMount(): void {
this.loadNavigationItems();
}
loadNavigationItems = async (): Promise<void> => {
this.setState({ isLoading: true });
Promise.all([this.navigationService.getNavigationItems()])
.then(navItems => this.setState({ navigationItems: navItems[0], navigationItemsToRender: [{ links: this.structureNavItems(navItems[0]) }]}))
.finally(() => this.setState({ isLoading: false }));
}
structureNavItems = (navItems: NavigationItemContract[]): INavLink[] => (
navItems.map(navItem => {
const newNode: INavLink = {
key: navItem.key,
name: navItem.label,
url: '',
targetKey: navItem.targetKey || '',
isExpanded: true
};
if (navItem.navigationItems) newNode.links = this.structureNavItems(navItem.navigationItems);
return newNode;
})
)
closePopUps = (): void => {
this.setState({ currentNavItem: null, showDeleteConfirmation: false, showNavigationItemModal: false });
this.loadNavigationItems();
}
deleteNavItem = async (): Promise<void> => {
const updatedNavItems = this.removeNavItem(this.state.navigationItems, this.state.currentNavItem.key);
await this.navigationService.updateNavigation(updatedNavItems);
this.eventManager.dispatchEvent('onSaveChanges');
this.closePopUps();
}
removeNavItem = (navItems: NavigationItemContract[], removableNavItemKey: string): NavigationItemContract[] => (
navItems.filter(navItem => {
const keep = navItem.key !== removableNavItemKey;
if (keep && navItem.navigationItems) {
navItem.navigationItems = this.removeNavItem(navItem.navigationItems, removableNavItemKey);
}
return keep;
})
)
findNavItemByKey = (navItems: NavigationItemContract[], value: string): NavigationItemContract => {
for (const obj of navItems) {
if (obj.key === value) {
return obj;
}
if (obj.navigationItems && obj.navigationItems.length > 0) {
const result = this.findNavItemByKey(obj.navigationItems, value);
if (result !== null) {
return result;
}
}
}
return null;
}
findParentItemKey = (array: NavigationItemContract[], childKey: string, parentKey: string = ''): string => {
for (const obj of array) {
if (obj.key === childKey) {
return parentKey;
}
if (obj.navigationItems && obj.navigationItems.length > 0) {
const nestedParentKey = this.findParentItemKey(obj.navigationItems, childKey, obj.key);
if (nestedParentKey !== null) {
return nestedParentKey;
}
}
}
return null;
}
moveItemUp = async (array: NavigationItemContract[], parentKey: string, itemKey: string): Promise<void> => {
const updatedArray = JSON.parse(JSON.stringify(array)); // Create a deep copy of the array
const moveItemUpRecursive = (items: NavigationItemContract[], parentKey: string) => {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.key === itemKey) {
// Move the item up within its parent's navigationItems array
if (i > 0) {
[items[i - 1], items[i]] = [items[i], items[i - 1]];
}
return true;
}
if (item.navigationItems) {
// Recursively search in nested items
if (moveItemUpRecursive(item.navigationItems, item.key)) {
return true;
}
}
}
return false;
};
moveItemUpRecursive(updatedArray, parentKey);
await this.navigationService.updateNavigation(updatedArray);
this.eventManager.dispatchEvent('onSaveChanges');
this.loadNavigationItems();
}
moveItemDown = async (array: NavigationItemContract[], parentKey: string, itemKey: string): Promise<void> => {
const updatedArray = JSON.parse(JSON.stringify(array)); // Create a deep copy of the array
const moveItemDownRecursive = (items, parentKey) => {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.key === itemKey) {
// Move the item down within its parent's navigationItems array
if (i < items.length - 1) {
[items[i], items[i + 1]] = [items[i + 1], items[i]];
}
return true;
}
if (item.navigationItems) {
// Recursively search in nested items
if (moveItemDownRecursive(item.navigationItems, item.key)) {
return true;
}
}
}
return false;
};
moveItemDownRecursive(updatedArray, parentKey);
await this.navigationService.updateNavigation(updatedArray);
this.eventManager.dispatchEvent('onSaveChanges');
this.loadNavigationItems();
}
checkItemPosition = (itemKey: string): ItemPosition => {
const navItems = this.state.navigationItems;
const parent = this.findNavItemByKey(navItems, this.findParentItemKey(navItems, itemKey));
let isFirst = null;
let isLast = null;
if (!parent) {
// Handle top-level items
isFirst = navItems[0].key === itemKey;
isLast = navItems[navItems.length - 1].key === itemKey;
}
if (parent && parent.navigationItems && parent.navigationItems.length > 0) {
isFirst = parent.navigationItems[0].key === itemKey;
isLast = parent.navigationItems[parent.navigationItems.length - 1].key === itemKey;
}
if (isFirst && isLast) {
return ItemPosition.FIRST_AND_LAST;
} else if (isFirst) {
return ItemPosition.FIRST;
} else if (isLast) {
return ItemPosition.LAST;
}
return null;
}
renderNavItemContent = (navItem: INavLink): JSX.Element => {
const itemPosition = this.checkItemPosition(navItem.key);
return (
<Stack horizontal horizontalAlign="space-between" className="nav-item-outer-stack">
<Text className="nav-item-name">{navItem.name}</Text>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }} className="nav-item-inner">
{(itemPosition !== ItemPosition.FIRST && itemPosition !== ItemPosition.FIRST_AND_LAST) &&
<FontIcon
iconName="ChevronUpMed"
title="Move up"
style={iconStyles}
onClick={(event) => {
event.stopPropagation();
this.moveItemUp(this.state.navigationItems, this.findParentItemKey(this.state.navigationItems, navItem.key), navItem.key)
}}
/>
}
{(itemPosition !== ItemPosition.LAST && itemPosition !== ItemPosition.FIRST_AND_LAST) &&
<FontIcon
iconName="ChevronDownMed"
title="Move down"
style={iconStyles}
onClick={(event) => {
event.stopPropagation();
this.moveItemDown(this.state.navigationItems, this.findParentItemKey(this.state.navigationItems, navItem.key), navItem.key)
}}
/>
}
<FontIcon
iconName="Settings"
title="Edit"
style={iconStyles}
onClick={(event) => {
event.stopPropagation();
this.setState({ showNavigationItemModal: true, currentNavItem: this.findNavItemByKey(this.state.navigationItems, navItem.key) })
}
}
/>
</Stack>
</Stack>
);
}
render(): JSX.Element {
return <>
{this.state.showDeleteConfirmation &&
<DeleteConfirmationOverlay
deleteItemTitle={this.state.currentNavItem.label}
onConfirm={this.deleteNavItem.bind(this)}
onDismiss={this.closePopUps.bind(this)}
/>
}
{this.state.showNavigationItemModal &&
<NavigationItemModal
navItem={this.state.currentNavItem}
navItems={this.state.navigationItems}
onDelete={this.deleteNavItem.bind(this)}
onDismiss={this.closePopUps.bind(this)}
/>
}
<>
<BackButton onClick={this.props.onBackButtonClick} />
<Stack className="nav-item-description-container">
<Text className="description-title">Site menu</Text>
<Text className="description-text">Manage and organize navigation of your developer portal. After you create a menu, you can add it to a page or layout with the "menu" widget.</Text>
</Stack>
<CommandBarButton
iconProps={addIcon}
text="Add item"
className="nav-item-list-button"
onClick={() => this.setState({ showNavigationItemModal: true, currentNavItem: null })}
/>
{this.state.isLoading && <Spinner />}
{this.state.navigationItemsToRender.length === 0 && !this.state.isLoading
? <Text block className="nav-item-description-container">It seems that you don't have site menu items yet. Would you like to create one?</Text>
:
<Nav
ariaLabel="Site menu"
groups={this.state.navigationItemsToRender}
onRenderLink={(item) => this.renderNavItemContent(item)}
/>
}
</>
</>
}
}