Skip to content

Commit

Permalink
Merge pull request #947 from Katli95/filepicker-defaultFolderPath
Browse files Browse the repository at this point in the history
Add support for a defaultFolderPath prop for the <FilePicker/>
  • Loading branch information
AJIXuMuK authored Jun 30, 2021
2 parents 4b7c170 + 44e59db commit 1dfa2e5
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 40 deletions.
3 changes: 2 additions & 1 deletion docs/documentation/docs/controls/FilePicker.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ The FilePicker component can be configured with the following properties:
| label | string | no | Specifies the text describing the file picker. |
| buttonLabel | string | no | Specifies the label of the file picker button. |
| buttonIcon | string | no | In case it is provided the file picker will be rendered as an action button. |
buttonIconProps | IIconProps | no | In case it is provided the file picker will be rendered as an Icon the and all can define Properties for Icon |
| buttonIconProps | IIconProps | no | In case it is provided the file picker will be rendered as an Icon the and all can define Properties for Icon |
| defaultFolderAbsolutePath | string | no | Optional string parameter to set a default active folder/library for the SiteFilesTab. E.g. `"https://contoso.sharepoint.com/teams/siteName/documentLibrary/Folder 1/SubFolder 1"` |
| onSave | (filePickerResult: IFilePickerResult[]) => void | yes | Handler when the file has been selected and picker has been closed. |
| onChange | (filePickerResult: IFilePickerResult[]) => void | no | Handler when the file selection has been changed. |
| onCancel | () => void | no | Handler when file picker has been cancelled. |
Expand Down
1 change: 1 addition & 0 deletions src/controls/filePicker/FilePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export class FilePicker extends React.Component<
<SiteFilePickerTab
fileBrowserService={this.fileBrowserService}
includePageLibraries={this.props.includePageLibraries}
defaultFolderAbsolutePath={this.props.defaultFolderAbsolutePath}
{...linkTabProps}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/controls/filePicker/FilePicker.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { BaseComponentContext } from '@microsoft/sp-component-base';
import { IBreadcrumbItem } from "office-ui-fabric-react/lib/Breadcrumb";
import { IFile, ILibrary } from "../../services/FileBrowserService.types";
import { IFile, IFolder, ILibrary } from "../../services/FileBrowserService.types";

export interface FilePickerBreadcrumbItem extends IBreadcrumbItem {
libraryData?: ILibrary;
folderData?: IFile;
folderData?: IFolder;
}

export interface IFilePickerTab {
Expand Down
5 changes: 5 additions & 0 deletions src/controls/filePicker/IFilePickerProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,9 @@ export interface IFilePickerProps {
* Specifies if Site Pages library to be visible on Sites tab
*/
includePageLibraries?: boolean;

/**
* Specifies a default folder to be active in the Site Files tab
*/
defaultFolderAbsolutePath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ export interface ISiteFilePickerTabProps extends IFilePickerTab {
*/
breadcrumbFirstNode?: IBreadcrumbItem;

/**
* Specifies a default folder to be active in the Site Files tab
*/
defaultFolderAbsolutePath?: string;

includePageLibraries?: boolean;
}
141 changes: 124 additions & 17 deletions src/controls/filePicker/SiteFilePickerTab/SiteFilePickerTab.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,153 @@
import * as React from 'react';
import findIndex from 'lodash/findIndex';
import { ISiteFilePickerTabProps } from './ISiteFilePickerTabProps';
import {ISiteFilePickerTabState } from './ISiteFilePickerTabState';
import { ISiteFilePickerTabState } from './ISiteFilePickerTabState';
import { DocumentLibraryBrowser } from '../controls/DocumentLibraryBrowser/DocumentLibraryBrowser';
import { FileBrowser } from '../controls/FileBrowser/FileBrowser';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb';
import { IFile, ILibrary } from '../../../services/FileBrowserService.types';
import { IFile, IFolder, ILibrary } from '../../../services/FileBrowserService.types';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { IFilePickerResult, FilePickerBreadcrumbItem } from '../FilePicker.types';

import { SPWeb } from "@microsoft/sp-page-context";

import styles from './SiteFilePickerTab.module.scss';
import * as strings from 'ControlStrings';
import { urlCombine } from '../../../common/utilities';
import { cloneDeep } from '@microsoft/sp-lodash-subset';

export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTabProps, ISiteFilePickerTabState> {
private _defaultLibraryNamePromise: Promise<void | string> = Promise.resolve();

constructor(props: ISiteFilePickerTabProps) {
super(props);

// Add current site to the breadcrumb or the provided node
const breadcrumbSiteNode: FilePickerBreadcrumbItem = this.props.breadcrumbFirstNode ? this.props. breadcrumbFirstNode : {
isCurrentItem: true,
const breadcrumbSiteNode: FilePickerBreadcrumbItem = this.props.breadcrumbFirstNode ? this.props.breadcrumbFirstNode : {
isCurrentItem: false,
text: props.context.pageContext.web.title,
key: props.context.pageContext.web.id.toString()
key: props.context.pageContext.web.id.toString(),
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
};
breadcrumbSiteNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbSiteNode); };

let breadcrumbItems: FilePickerBreadcrumbItem[] = [breadcrumbSiteNode];

let { folderAbsPath = undefined, libraryServRelUrl = undefined, folderServRelPath = undefined, folderBreadcrumbs = [] } = props.defaultFolderAbsolutePath
? this._parseInitialLocationState(
props.defaultFolderAbsolutePath,
props.context.pageContext.web
)
: {};

breadcrumbItems.push(...folderBreadcrumbs);

breadcrumbItems[breadcrumbItems.length - 1].isCurrentItem = true;

this.state = {
filePickerResult: null,
libraryAbsolutePath: undefined,
libraryUrl: urlCombine(props.context.pageContext.web.serverRelativeUrl, '/Shared%20Documents'),
libraryPath: undefined,
libraryAbsolutePath: folderAbsPath || undefined,
libraryUrl: libraryServRelUrl || urlCombine(props.context.pageContext.web.serverRelativeUrl, '/Shared%20Documents'),
libraryPath: folderServRelPath,
folderName: strings.DocumentLibraries,
breadcrumbItems: [breadcrumbSiteNode]
breadcrumbItems
};
}

private _parseInitialLocationState(folderAbsPath: string, { serverRelativeUrl: webServRelUrl, absoluteUrl: webAbsUrl }: SPWeb) {
// folderAbsPath: "https://tenant.sharepoint.com/teams/Test/DocLib/Folder"

// folderServRelPath: "/teams/Test/DocLib/Folder"
let folderServRelPath = folderAbsPath && folderAbsPath.substr(folderAbsPath.indexOf(webServRelUrl));

// folderWebRelPath: "/DocLib/Folder"
let folderWebRelPath = folderServRelPath && folderServRelPath.substr(webServRelUrl.length);
let libInternalName = folderWebRelPath && folderWebRelPath.substring(1, Math.max(folderWebRelPath.indexOf("/", 2), 0) || undefined)

// libraryServRelUrl: "/teams/Test/DocLib/"
let libraryServRelUrl = urlCombine(webServRelUrl, libInternalName);

let tenantUrl = folderAbsPath.substring(0, folderAbsPath.indexOf(webServRelUrl));
let folderBreadcrumbs: FilePickerBreadcrumbItem[] = this.parseBreadcrumbsFromPaths(
libraryServRelUrl,
folderServRelPath,
folderWebRelPath,
webAbsUrl,
tenantUrl,
libInternalName
);

return { libraryServRelUrl, folderServRelPath, folderAbsPath, folderBreadcrumbs };
}

private parseBreadcrumbsFromPaths(
libraryServRelUrl: string,
folderServRelPath: string,
folderWebRelPath: string,
webAbsUrl: string,
tenantUrl: string,
libInternalName: string
) {
this._defaultLibraryNamePromise = this.props.fileBrowserService.getLibraryNameByInternalName(libInternalName);
let folderBreadcrumbs: FilePickerBreadcrumbItem[] = [];
folderBreadcrumbs.push({
isCurrentItem: false,
text: libInternalName,
key: libraryServRelUrl,
libraryData: {
serverRelativeUrl: libraryServRelUrl,
absoluteUrl: urlCombine(webAbsUrl, libInternalName),
title: libInternalName
},
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
});

if (folderServRelPath != libraryServRelUrl) {
let folderLibRelPath = folderWebRelPath.substring(libInternalName.length + 2);
let breadcrumbFolderServRelPath = libraryServRelUrl;

let crumbs: FilePickerBreadcrumbItem[] = folderLibRelPath.split("/").map((currFolderName => {
breadcrumbFolderServRelPath += `/${currFolderName}`;
return {
isCurrentItem: false,
text: currFolderName,
key: urlCombine(tenantUrl, breadcrumbFolderServRelPath),
folderData: {
name: currFolderName,
absoluteUrl: urlCombine(tenantUrl, breadcrumbFolderServRelPath),
serverRelativeUrl: breadcrumbFolderServRelPath,
},
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
};
}));

folderBreadcrumbs.push(...crumbs);
}
return folderBreadcrumbs;
}

public componentDidMount(): void {
this._defaultLibraryNamePromise.then(docLibName => {
if (docLibName) {
let updatedBCItems = cloneDeep(this.state.breadcrumbItems);
updatedBCItems.forEach(crumb => {
if (crumb.libraryData) {
crumb.text = docLibName;
crumb.libraryData.title = docLibName;
}
});
this.setState({ breadcrumbItems: updatedBCItems });
}
}).catch((err) => {
console.log("[SiteFilePicker] Failed To Fetch defaultLibraryName, defaulting to internal name");
});
}

public render(): React.ReactElement<ISiteFilePickerTabProps> {
return (
<div className={styles.tabContainer}>
<div className={styles.tabContainer} >
<div className={styles.tabHeaderContainer}>
<Breadcrumb items={this.state.breadcrumbItems} /*onRenderItem={this.renderBreadcrumbItem}*/ className={styles.breadcrumbNav}/>
<Breadcrumb items={this.state.breadcrumbItems} /*onRenderItem={this.renderBreadcrumbItem}*/ className={styles.breadcrumbNav} />
</div>
<div className={styles.tabFiles}>
{this.state.libraryAbsolutePath === undefined &&
Expand Down Expand Up @@ -142,7 +249,7 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
/**
* Triggered when user opens a file folder
*/
private _handleOpenFolder = (folder: IFile, addBreadcrumbNode: boolean) => {
private _handleOpenFolder = (folder: IFolder, addBreadcrumbNode: boolean) => {
const { breadcrumbItems } = this.state;

if (addBreadcrumbNode) {
Expand All @@ -151,9 +258,9 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
folderData: folder,
isCurrentItem: true,
text: folder.name,
key: folder.absoluteUrl
key: folder.absoluteUrl,
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
};
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); };
breadcrumbItems.push(breadcrumbNode);
}

Expand All @@ -177,9 +284,9 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
libraryData: library,
isCurrentItem: true,
text: library.title,
key: library.serverRelativeUrl
key: library.serverRelativeUrl,
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
};
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); };
breadcrumbItems.push(breadcrumbNode);
}
this.setState({
Expand Down
26 changes: 25 additions & 1 deletion src/services/FileBrowserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,30 @@ export class FileBrowserService {
}
}

/**
* Gets document and media libraries from the site
*/
public getLibraryNameByInternalName = async (internalName: string): Promise<string> => {
try {
const absoluteUrl = this.context.pageContext.web.absoluteUrl;
const restApi = `${absoluteUrl}/_api/web/GetFolderByServerRelativeUrl('${internalName}')/Properties?$select=vti_x005f_listtitle`;
const libraryResult = await this.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1);

if (!libraryResult || !libraryResult.ok) {
throw new Error(`Something went wrong when executing request. Status='${libraryResult.status}'`);
}
const libResults: { vti_x005f_listtitle: string } = await libraryResult.json();
if (!libResults || !libResults.vti_x005f_listtitle) {
throw new Error(`Cannot read data from the results.`);
}

return libResults.vti_x005f_listtitle != internalName && libResults.vti_x005f_listtitle || "";
} catch (error) {
console.error(`[FileBrowserService.getSiteLibraryNameByInternalName]: Err='${error.message}'`);
return null;
}
}

/**
* Downloads document content from SP location.
*/
Expand Down Expand Up @@ -117,7 +141,7 @@ export class FileBrowserService {
}
};
if (folderPath) {
body.parameters["FolderServerRelativeUrl"] = folderPath;
body.parameters["FolderServerRelativeUrl"] = folderPath;
}
const data: any = await this.context.spHttpClient.fetch(restApi, SPHttpClient.configurations.v1, {
method: "POST",
Expand Down
4 changes: 4 additions & 0 deletions src/services/FileBrowserService.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export interface IFile {
supportsThumbnail: boolean;
}

export interface IFolder extends Pick<IFile, "name" | "absoluteUrl" | "serverRelativeUrl"> {

}

export interface ILibrary {
title: string;
absoluteUrl: string;
Expand Down
42 changes: 24 additions & 18 deletions src/webparts/controlsTest/components/ControlsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,12 @@ import {
import { MyTeams } from "../../../controls/MyTeams";
import { TeamPicker } from "../../../TeamPicker";
import { TeamChannelPicker } from "../../../TeamChannelPicker";
import {​​ DragDropFiles }​​ from "../../../DragDropFiles";
import {​​ SitePicker }​​ from "../../../controls/sitePicker/SitePicker";
import { DragDropFiles } from "../../../DragDropFiles";
import { SitePicker } from "../../../controls/sitePicker/SitePicker";
import { DynamicForm } from '../../../controls/dynamicForm';
import { LocationPicker } from "../../../controls/locationPicker/LocationPicker";
import { ILocationPickerItem } from "../../../controls/locationPicker/ILocationPicker";
import { debounce } from "lodash";



Expand Down Expand Up @@ -854,7 +855,7 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
<Link href="https://pnp.github.io/sp-dev-fx-controls-react/">See all</Link>
} />

<Stack styles={{ root: { marginBottom: 200 } }}>
<Stack styles={{ root: { marginBottom: 200 } }}>
<MyTeams
title="My Teams"
webPartContext={this.props.context}
Expand Down Expand Up @@ -1274,12 +1275,12 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
iconName="Upload"
labelMessage="My custom upload File"
>
<Placeholder iconName='BulkUpload'
iconText='Drag files or folder with files here...'
description={defaultClassNames => <span className={defaultClassNames}>Drag files or folder with files here...</span>}
buttonLabel='Configure'
hideButton={this.props.displayMode === DisplayMode.Read}
onConfigure={this._onConfigure} />
<Placeholder iconName='BulkUpload'
iconText='Drag files or folder with files here...'
description={defaultClassNames => <span className={defaultClassNames}>Drag files or folder with files here...</span>}
buttonLabel='Configure'
hideButton={this.props.displayMode === DisplayMode.Read}
onConfigure={this._onConfigure} />
</DragDropFiles>
<br></br>

Expand Down Expand Up @@ -1387,14 +1388,14 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC

<div className="ms-font-m">Site picker tester:
<SitePicker
context={this.props.context}
label={'select sites'}
mode={'site'}
allowSearch={true}
multiSelect={false}
onChange={(sites) => { console.log(sites); }}
placeholder={'Select sites'}
searchPlaceholder={'Filter sites'} />
context={this.props.context}
label={'select sites'}
mode={'site'}
allowSearch={true}
multiSelect={false}
onChange={(sites) => { console.log(sites); }}
placeholder={'Select sites'}
searchPlaceholder={'Filter sites'} />
</div>

<div className="ms-font-m">List picker tester:
Expand Down Expand Up @@ -1596,11 +1597,16 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC

<div>
<h3>File Picker</h3>
<TextField
label="Default SiteFileTab Folder"
onChange={debounce((ev, newVal) => { this.setState({ filePickerDefaultFolderAbsolutePath: newVal }); }, 500)}
styles={{ root: { marginBottom: 10 } }}
/>
<FilePicker
bingAPIKey="<BING API KEY>"
defaultFolderAbsolutePath={this.state.filePickerDefaultFolderAbsolutePath}
//accepts={[".gif", ".jpg", ".jpeg", ".bmp", ".dib", ".tif", ".tiff", ".ico", ".png", ".jxr", ".svg"]}
buttonLabel="Add File"

buttonIconProps={{ iconName: 'Add', styles: { root: { fontSize: 42 } } }}
onSave={this._onFilePickerSave}
onChange={(filePickerResult: IFilePickerResult[]) => { console.log(filePickerResult); }}
Expand Down
2 changes: 1 addition & 1 deletion src/webparts/controlsTest/components/IControlsTestProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ export interface IControlsTestState {
showErrorDialog?: boolean;
selectedTeam:ITag[];
selectedTeamChannels:ITag[];

filePickerDefaultFolderAbsolutePath?: string;
}

0 comments on commit 1dfa2e5

Please sign in to comment.