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

v2.9.0 merge #1000

Merged
merged 26 commits into from
Aug 25, 2021
Merged
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Ketill authored and AJIXuMuK committed Jun 30, 2021
commit 00e4991ef5aeaaf3b77188bfb90ce42076eaad22
3 changes: 2 additions & 1 deletion docs/documentation/docs/controls/FilePicker.md
Original file line number Diff line number Diff line change
@@ -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. |
1 change: 1 addition & 0 deletions src/controls/filePicker/FilePicker.tsx
Original file line number Diff line number Diff line change
@@ -214,6 +214,7 @@ export class FilePicker extends React.Component<
<SiteFilePickerTab
fileBrowserService={this.fileBrowserService}
includePageLibraries={this.props.includePageLibraries}
defaultFolderAbsolutePath={this.props.defaultFolderAbsolutePath}
{...linkTabProps}
/>
)}
4 changes: 2 additions & 2 deletions src/controls/filePicker/FilePicker.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { WebPartContext } from "@microsoft/sp-webpart-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";
import { ExtensionContext } from "@microsoft/sp-extension-base";

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

export interface IFilePickerTab {
5 changes: 5 additions & 0 deletions src/controls/filePicker/IFilePickerProps.ts
Original file line number Diff line number Diff line change
@@ -157,4 +157,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
@@ -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 &&
@@ -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) {
@@ -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);
}

@@ -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({
26 changes: 25 additions & 1 deletion src/services/FileBrowserService.ts
Original file line number Diff line number Diff line change
@@ -80,6 +80,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.
*/
@@ -118,7 +142,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",
4 changes: 4 additions & 0 deletions src/services/FileBrowserService.types.ts
Original file line number Diff line number Diff line change
@@ -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;
12 changes: 8 additions & 4 deletions src/webparts/controlsTest/components/ControlsTest.tsx
Original file line number Diff line number Diff line change
@@ -167,6 +167,7 @@ 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";

// Used to render document card
/**
@@ -1210,14 +1211,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} />

</DragDropFiles>
<br></br>

@@ -1322,7 +1321,7 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
</div>

<div className="ms-font-m">Site picker tester:
<SitePicker
<SitePicker
context={this.props.context}
label={'select sites'}
mode={'site'}
@@ -1531,11 +1530,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); }}
1 change: 1 addition & 0 deletions src/webparts/controlsTest/components/IControlsTestProps.ts
Original file line number Diff line number Diff line change
@@ -35,4 +35,5 @@ export interface IControlsTestState {
showCustomisedAnimatedDialog?: boolean;
showSuccessDialog?: boolean;
showErrorDialog?: boolean;
filePickerDefaultFolderAbsolutePath?: string;
}