Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add breadcrumb component for spo entities #3455

Merged
merged 19 commits into from
Feb 5, 2024
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 49 additions & 1 deletion docs/extensibility/web_components_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Here are the list of all **reusable** web components you can use to customize yo
- [<pnp-collapsible>](#ltpnp-collapsiblegt)
- [<pnp-persona>](#ltpnp-personagt)
- [<pnp-img>](#ltpnp-imggt)
- [<pnp-breadcrumb>](#ltpnp-breadcrumbgt)

> All other web components you will see in builtin layout templates are considered **internal** and are not supported for custom use.

Expand Down Expand Up @@ -216,4 +217,51 @@ Here are the list of all **reusable** web components you can use to customize yo
| Parameter | Description |
| --------- | ----------- |
|**errorImage**|URL to the fallback image
|**hideOnError**|Hide image on error
|**hideOnError**|Hide image on error

## `<pnp-breadcrumb>`
- **Description**: Render a breadcrumb path of a SharePoint entity (file, item, folder, document library etc.).

!["Breadcrumb component"](../assets/extensibility/web_components/breadcrumb_component.png){: .center}

- **Usage**:

Get started with:
```html
<pnp-breadcrumb
data-path="{{OriginalPath}}"
data-site-url="{{SPSiteURL}}"
data-web-url="{{SPWebUrl}}"
data-entity-title="{{Title}}"
data-entity-file-type="{{FileType}}"
/>
```
Use all properties:
```html
<pnp-breadcrumb
data-path="{{OriginalPath}}"
data-site-url="{{SPSiteURL}}"
data-web-url="{{SPWebUrl}}"
data-entity-title="{{Title}}"
data-entity-file-type="{{FileType}}"
data-include-site-name="false"
data-include-entity-name="true"
data-breadcrumb-items-as-links="true"
data-max-displayed-items="3"
data-overflow-index="0"
data-font-size="12"
/>
```
|Parameter|Description|
|--|--|
|data-path|Used for creating the breadcrumb path. Component is designed to receive `OriginalPath` or `Path` property. Property is required for rendering the breadcrumb path. `String`|
|data-site-url|Used for creating the breadcrumb path. Component is designed to receive `SPSiteURL` property. Property is required for rendering the breadcrumb path. `String`|
|data-web-url|Used for creating the breadcrumb path. Component is designed to receive `SPWebUrl` property. Property is required for rendering the breadcrumb path. `String`|
|data-entity-title|Used for creating the breadcrumb path. Component is designed to receive `Title` property. Property is required for rendering the breadcrumb path. `String`|
|data-entity-file-type|Used for creating the breadcrumb path. Component is designed to receive `FileType` property. Property is required for rendering the breadcrumb path. `String`|
|data-include-site-name|If the site name should be included in the breadcrumb items. Optional, default value `true`. `Boolean`|
|data-include-entity-name|If the entity name should be included in the breadcrumb items. If the value is set to `false`, not only is the entity name excluded from the breadcrumb path, but also the last item in the breadcrumb path is not highlighted in bold. Optional, default value `true`. `Boolean`|
|data-breadcrumb-items-as-links|If the breadcrumb items should be clickable links to the path they represent. Optional, default value `true`. `Boolean`|
|data-max-displayed-items|The maximum number of breadcrumb items to display before coalescing. If not specified, all breadcrumbs will be rendered. Optional, default value `3`. `Int`|
|data-overflow-index| Index where overflow items will be collapsed. Optional, default value `0`. `Int`|
|data-font-size|Font size of breadcrumb items. Optional, default value `12`. `Int`|
5 changes: 5 additions & 0 deletions search-parts/src/components/AvailableComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ImageWebComponent} from './ImageComponent';
import { ItemSelectionWebComponent } from './ItemSelectionComponent';
import { FilterSearchBoxWebComponent } from './filters/FilterSearchBoxComponent';
import { FilterValueOperatorWebComponent } from './filters/FilterValueOperatorComponent';
import { SpoPathBreadcrumbWebComponent } from './SpoPathBreadcrumbComponent';
import { SortWebComponent } from './SortComponent';

export class AvailableComponents {
Expand Down Expand Up @@ -122,6 +123,10 @@ export class AvailableComponents {
componentName: "pnp-filteroperator",
componentClass: FilterValueOperatorWebComponent
},
{
componentName: "pnp-breadcrumb",
componentClass: SpoPathBreadcrumbWebComponent
},
{
componentName: 'pnp-sortfield',
componentClass: SortWebComponent
Expand Down
216 changes: 216 additions & 0 deletions search-parts/src/components/SpoPathBreadcrumbComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import * as React from 'react';
import { BaseWebComponent } from '@pnp/modern-search-extensibility';
import * as ReactDOM from 'react-dom';
import { ITheme, Breadcrumb, IBreadcrumbItem } from '@fluentui/react';
import { IReadonlyTheme } from '@microsoft/sp-component-base';

export interface IBreadcrumbProps {
/**
* Path from which breadcrumb items are formed from. Ideally use the path property of a SharePoint document, list item, folder, etc.
*/
path?: string;

/**
* The SharePoint site URL from which the entity path originates from.
*/
siteUrl?: string;

/**
* The SharePoint web URL from which the entity path originates from.
*/
webUrl?: string;

/**
* Title of the entity for which the breadcrumb path is generated.
*/
entityTitle?: string;

/**
* File type of the entity for which the breadcrumb path is generated.
*/
entityFileType?: string;

/**
* Determines whether the site name should be included in the breadcrumb items.
*/
includeSiteName?: boolean;

/**
* Determines whether the entity name should be included in the breadcrumb items.
*/
includeEntityName?: boolean;

/**
* Determines whether the breadcrumb items should be clickable links to the path they represent.
*/
breadcrumbItemsAsLinks?: boolean;

/**
* The maximum number of breadcrumb items to display before coalescing. If not specified, all breadcrumbs will be rendered.
*/
maxDisplayedItems?: number;

/**
* Index where overflow items will be collapsed.
*/
overflowIndex?: number;

/**
* Font size of breadcrumb items.
*/
fontSize?: number;

/**
* The current theme settings.
*/
themeVariant?: IReadonlyTheme;
}

export interface IBreadcrumbState { }

// For example list items and images have DispForm.aspx?ID=xxxx in their path. This regex is used to check if the path contains DispForm.aspx?ID=xxxx
const DISP_FORM_REGEX = /DispForm\.aspx\?ID=\d+/;

export class SpoPathBreadcrumb extends React.Component<IBreadcrumbProps, IBreadcrumbState> {

static defaultProps = {
includeSiteName: true,
includeEntityName: true,
breadcrumbItemsAsLinks: true,
maxDisplayedItems: 3,
overflowIndex: 0,
fontSize: 12
};

public render() {
const { path, siteUrl, webUrl, entityTitle, entityFileType, includeSiteName, includeEntityName, breadcrumbItemsAsLinks, maxDisplayedItems, overflowIndex, fontSize, themeVariant } = this.props;

const commonStyles = {
fontSize: `${fontSize}px`,
padding: '1px',
selectors: {
// If the entity name is not included in path then reset the formatting of last breadcrumb item.
...( !includeEntityName ? { '&:last-child': { fontWeight: 'unset !important', color: 'unset !important' } } : {} )
},
};

const breadcrumbStyles = {
root: { margin: '0' },
item: { ...commonStyles },
itemLink: {
...commonStyles,
selectors: {
'&:hover': { backgroundColor: 'unset', textDecoration: 'underline'},
...commonStyles.selectors
},
},
};

const breadcrumbItems = this.validateEntityProps(path, siteUrl, entityTitle) ? this.getBreadcrumbItems(path, siteUrl, webUrl, entityTitle, entityFileType, includeSiteName, includeEntityName, breadcrumbItemsAsLinks) : undefined;

return (
<>
{breadcrumbItems !== undefined &&
<Breadcrumb
items={breadcrumbItems}
maxDisplayedItems={maxDisplayedItems}
overflowIndex={breadcrumbItems.length <= overflowIndex ? 0 : overflowIndex}
styles={breadcrumbStyles}
ariaLabel="Breadcrumb path"
overflowAriaLabel="More links"
theme={themeVariant as ITheme}
/>
}
</>
)
}

private validateEntityProps = (path: string, siteUrl: string, entityTitle: string): boolean => {
return path !== undefined && path !== null
&& siteUrl !== undefined && siteUrl !== null
&& entityTitle !== undefined && entityTitle !== null;
}

private getBreadcrumbItems = (path: string, siteUrl: string, webUrl: string, entityTitle: string, entityFileType: string, includeSiteName: boolean, includeEntityName: boolean, breadcrumbItemsAsLinks: boolean): IBreadcrumbItem[] => {
// Example:
// webUrl: https://contoso.sharepoint.com/sites/sitename/subsite
// path: https://contoso.sharepoint.com/sites/sitename/subsite/Shared Documents/Document.docx

// All entities don't have a webUrl property (e.g. list and doc libs). Therefore webUrl is not part of props validation and undefined/null check is needed here.
if (webUrl === undefined || webUrl === null) webUrl = siteUrl;

const frags = webUrl.split('/');
// frags: ["https:", "", "contoso.sharepoint.com", "sites", "sitename", "subsite"]

const isRootSite = siteUrl.split('/').length === 3;
// Root site only contains parts: ["https:", "", "contoso.sharepoint.com"]

const basePath = isRootSite ? frags.slice(0, 3).join('/') : frags.slice(0, 4).join('/');
// basePath: https://contoso.sharepoint.com/sites
// Root site base path: https://contoso.sharepoint.com

wobba marked this conversation as resolved.
Show resolved Hide resolved
// If includeSiteName is true, then only remove the base path from the original path. In example first items of path are "sitename", "subsite"
// If includeSiteName is false, then remove the whole webUrl from the original path. In example first item of path is "Shared Documents"
const replacePath = includeSiteName ? basePath : webUrl;
const parts = path.replace(replacePath, '').split('/').filter(part => part);

// If includeEntityName is false, then remove the last part of the path. In example remove Document.doxc. Last part is a title of the entity for which the breadcrumb path is generated
if (!includeEntityName) parts.pop();

const breadcrumbItems: IBreadcrumbItem[] = parts.map((part, index) => {
// If the current part is the last part of the path and it contains DispForm.aspx?ID=xxxx, then set the breadcrumb item text as entity title + optionally file type
const itemText = index+1 === parts.length && includeEntityName && DISP_FORM_REGEX.test(part) ?
entityFileType !== undefined && entityFileType !== null ?
`${entityTitle}.${entityFileType}`
: entityTitle
: part;

const item: IBreadcrumbItem = {
text: itemText,
key: `item${index + 1}`
};

// If breadcrumbItemsAsLinks is true, then add the href property to the breadcrumb item
if (breadcrumbItemsAsLinks) {
const relativePath = parts.slice(0, index + 1).join('/');

// If includeSiteName is true, then the href is the base path + the current path part because parts contain the site name and possible subsite(s)
// If includeSiteName is false, then the href is the webUrl + the current path part because parts do not contain the site name and possible subsite(s)
item.href = includeSiteName ? `${basePath}/${relativePath}` : `${webUrl}/${relativePath}`;
}

return item;
});

// If entity is located on the root site, then add the root site as first breadcrumb item if includeSiteName is true
if (isRootSite && includeSiteName) {
const item: IBreadcrumbItem = {
text: 'Home',
key: 'home'
};

if (breadcrumbItemsAsLinks) item.href = siteUrl;

breadcrumbItems.unshift(item);
}

return breadcrumbItems;
}
}

export class SpoPathBreadcrumbWebComponent extends BaseWebComponent {

public constructor() {
super();
}

public async connectedCallback() {
let props = this.resolveAttributes();
const spoPathBreadcrumb = <div style={{ display: 'flex' }}><SpoPathBreadcrumb {...props} /></div>;
ReactDOM.render(spoPathBreadcrumb, this);
}

protected onDispose(): void {
ReactDOM.unmountComponentAtNode(this);
}
}