Skip to content

Commit

Permalink
Merge pull request #3455 from lauribohm/breadcrumbs-component
Browse files Browse the repository at this point in the history
add breadcrumb component for spo entities
  • Loading branch information
wobba authored Feb 5, 2024
2 parents 42e83ff + 71d62d0 commit e7d24f2
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 1 deletion.
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

// 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);
}
}

0 comments on commit e7d24f2

Please sign in to comment.