Skip to content

Commit 00e4991

Browse files
KetillAJIXuMuK
authored andcommitted
1 parent 638c690 commit 00e4991

File tree

10 files changed

+177
-25
lines changed

10 files changed

+177
-25
lines changed

docs/documentation/docs/controls/FilePicker.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ The FilePicker component can be configured with the following properties:
7575
| label | string | no | Specifies the text describing the file picker. |
7676
| buttonLabel | string | no | Specifies the label of the file picker button. |
7777
| buttonIcon | string | no | In case it is provided the file picker will be rendered as an action button. |
78-
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 |
78+
| 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 |
79+
| 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"` |
7980
| onSave | (filePickerResult: IFilePickerResult[]) => void | yes | Handler when the file has been selected and picker has been closed. |
8081
| onChange | (filePickerResult: IFilePickerResult[]) => void | no | Handler when the file selection has been changed. |
8182
| onCancel | () => void | no | Handler when file picker has been cancelled. |

src/controls/filePicker/FilePicker.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ export class FilePicker extends React.Component<
214214
<SiteFilePickerTab
215215
fileBrowserService={this.fileBrowserService}
216216
includePageLibraries={this.props.includePageLibraries}
217+
defaultFolderAbsolutePath={this.props.defaultFolderAbsolutePath}
217218
{...linkTabProps}
218219
/>
219220
)}

src/controls/filePicker/FilePicker.types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { WebPartContext } from "@microsoft/sp-webpart-base";
22
import { IBreadcrumbItem } from "office-ui-fabric-react/lib/Breadcrumb";
3-
import { IFile, ILibrary } from "../../services/FileBrowserService.types";
3+
import { IFile, IFolder, ILibrary } from "../../services/FileBrowserService.types";
44
import { ExtensionContext } from "@microsoft/sp-extension-base";
55

66
export interface FilePickerBreadcrumbItem extends IBreadcrumbItem {
77
libraryData?: ILibrary;
8-
folderData?: IFile;
8+
folderData?: IFolder;
99
}
1010

1111
export interface IFilePickerTab {

src/controls/filePicker/IFilePickerProps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,9 @@ export interface IFilePickerProps {
157157
* Specifies if Site Pages library to be visible on Sites tab
158158
*/
159159
includePageLibraries?: boolean;
160+
161+
/**
162+
* Specifies a default folder to be active in the Site Files tab
163+
*/
164+
defaultFolderAbsolutePath?: string;
160165
}

src/controls/filePicker/SiteFilePickerTab/ISiteFilePickerTabProps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,10 @@ export interface ISiteFilePickerTabProps extends IFilePickerTab {
1010
*/
1111
breadcrumbFirstNode?: IBreadcrumbItem;
1212

13+
/**
14+
* Specifies a default folder to be active in the Site Files tab
15+
*/
16+
defaultFolderAbsolutePath?: string;
17+
1318
includePageLibraries?: boolean;
1419
}

src/controls/filePicker/SiteFilePickerTab/SiteFilePickerTab.tsx

Lines changed: 124 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,153 @@
11
import * as React from 'react';
22
import findIndex from 'lodash/findIndex';
33
import { ISiteFilePickerTabProps } from './ISiteFilePickerTabProps';
4-
import {ISiteFilePickerTabState } from './ISiteFilePickerTabState';
4+
import { ISiteFilePickerTabState } from './ISiteFilePickerTabState';
55
import { DocumentLibraryBrowser } from '../controls/DocumentLibraryBrowser/DocumentLibraryBrowser';
66
import { FileBrowser } from '../controls/FileBrowser/FileBrowser';
77
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
88
import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb';
9-
import { IFile, ILibrary } from '../../../services/FileBrowserService.types';
9+
import { IFile, IFolder, ILibrary } from '../../../services/FileBrowserService.types';
1010
import { Link } from 'office-ui-fabric-react/lib/Link';
1111
import { IFilePickerResult, FilePickerBreadcrumbItem } from '../FilePicker.types';
1212

13+
import { SPWeb } from "@microsoft/sp-page-context";
14+
1315
import styles from './SiteFilePickerTab.module.scss';
1416
import * as strings from 'ControlStrings';
1517
import { urlCombine } from '../../../common/utilities';
18+
import { cloneDeep } from '@microsoft/sp-lodash-subset';
1619

1720
export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTabProps, ISiteFilePickerTabState> {
21+
private _defaultLibraryNamePromise: Promise<void | string> = Promise.resolve();
22+
1823
constructor(props: ISiteFilePickerTabProps) {
1924
super(props);
2025

2126
// Add current site to the breadcrumb or the provided node
22-
const breadcrumbSiteNode: FilePickerBreadcrumbItem = this.props.breadcrumbFirstNode ? this.props. breadcrumbFirstNode : {
23-
isCurrentItem: true,
27+
const breadcrumbSiteNode: FilePickerBreadcrumbItem = this.props.breadcrumbFirstNode ? this.props.breadcrumbFirstNode : {
28+
isCurrentItem: false,
2429
text: props.context.pageContext.web.title,
25-
key: props.context.pageContext.web.id.toString()
30+
key: props.context.pageContext.web.id.toString(),
31+
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
2632
};
27-
breadcrumbSiteNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbSiteNode); };
33+
34+
let breadcrumbItems: FilePickerBreadcrumbItem[] = [breadcrumbSiteNode];
35+
36+
let { folderAbsPath = undefined, libraryServRelUrl = undefined, folderServRelPath = undefined, folderBreadcrumbs = [] } = props.defaultFolderAbsolutePath
37+
? this._parseInitialLocationState(
38+
props.defaultFolderAbsolutePath,
39+
props.context.pageContext.web
40+
)
41+
: {};
42+
43+
breadcrumbItems.push(...folderBreadcrumbs);
44+
45+
breadcrumbItems[breadcrumbItems.length - 1].isCurrentItem = true;
2846

2947
this.state = {
3048
filePickerResult: null,
31-
libraryAbsolutePath: undefined,
32-
libraryUrl: urlCombine(props.context.pageContext.web.serverRelativeUrl, '/Shared%20Documents'),
33-
libraryPath: undefined,
49+
libraryAbsolutePath: folderAbsPath || undefined,
50+
libraryUrl: libraryServRelUrl || urlCombine(props.context.pageContext.web.serverRelativeUrl, '/Shared%20Documents'),
51+
libraryPath: folderServRelPath,
3452
folderName: strings.DocumentLibraries,
35-
breadcrumbItems: [breadcrumbSiteNode]
53+
breadcrumbItems
3654
};
3755
}
3856

57+
private _parseInitialLocationState(folderAbsPath: string, { serverRelativeUrl: webServRelUrl, absoluteUrl: webAbsUrl }: SPWeb) {
58+
// folderAbsPath: "https://tenant.sharepoint.com/teams/Test/DocLib/Folder"
59+
60+
// folderServRelPath: "/teams/Test/DocLib/Folder"
61+
let folderServRelPath = folderAbsPath && folderAbsPath.substr(folderAbsPath.indexOf(webServRelUrl));
62+
63+
// folderWebRelPath: "/DocLib/Folder"
64+
let folderWebRelPath = folderServRelPath && folderServRelPath.substr(webServRelUrl.length);
65+
let libInternalName = folderWebRelPath && folderWebRelPath.substring(1, Math.max(folderWebRelPath.indexOf("/", 2), 0) || undefined)
66+
67+
// libraryServRelUrl: "/teams/Test/DocLib/"
68+
let libraryServRelUrl = urlCombine(webServRelUrl, libInternalName);
69+
70+
let tenantUrl = folderAbsPath.substring(0, folderAbsPath.indexOf(webServRelUrl));
71+
let folderBreadcrumbs: FilePickerBreadcrumbItem[] = this.parseBreadcrumbsFromPaths(
72+
libraryServRelUrl,
73+
folderServRelPath,
74+
folderWebRelPath,
75+
webAbsUrl,
76+
tenantUrl,
77+
libInternalName
78+
);
79+
80+
return { libraryServRelUrl, folderServRelPath, folderAbsPath, folderBreadcrumbs };
81+
}
82+
83+
private parseBreadcrumbsFromPaths(
84+
libraryServRelUrl: string,
85+
folderServRelPath: string,
86+
folderWebRelPath: string,
87+
webAbsUrl: string,
88+
tenantUrl: string,
89+
libInternalName: string
90+
) {
91+
this._defaultLibraryNamePromise = this.props.fileBrowserService.getLibraryNameByInternalName(libInternalName);
92+
let folderBreadcrumbs: FilePickerBreadcrumbItem[] = [];
93+
folderBreadcrumbs.push({
94+
isCurrentItem: false,
95+
text: libInternalName,
96+
key: libraryServRelUrl,
97+
libraryData: {
98+
serverRelativeUrl: libraryServRelUrl,
99+
absoluteUrl: urlCombine(webAbsUrl, libInternalName),
100+
title: libInternalName
101+
},
102+
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
103+
});
104+
105+
if (folderServRelPath != libraryServRelUrl) {
106+
let folderLibRelPath = folderWebRelPath.substring(libInternalName.length + 2);
107+
let breadcrumbFolderServRelPath = libraryServRelUrl;
108+
109+
let crumbs: FilePickerBreadcrumbItem[] = folderLibRelPath.split("/").map((currFolderName => {
110+
breadcrumbFolderServRelPath += `/${currFolderName}`;
111+
return {
112+
isCurrentItem: false,
113+
text: currFolderName,
114+
key: urlCombine(tenantUrl, breadcrumbFolderServRelPath),
115+
folderData: {
116+
name: currFolderName,
117+
absoluteUrl: urlCombine(tenantUrl, breadcrumbFolderServRelPath),
118+
serverRelativeUrl: breadcrumbFolderServRelPath,
119+
},
120+
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
121+
};
122+
}));
123+
124+
folderBreadcrumbs.push(...crumbs);
125+
}
126+
return folderBreadcrumbs;
127+
}
128+
129+
public componentDidMount(): void {
130+
this._defaultLibraryNamePromise.then(docLibName => {
131+
if (docLibName) {
132+
let updatedBCItems = cloneDeep(this.state.breadcrumbItems);
133+
updatedBCItems.forEach(crumb => {
134+
if (crumb.libraryData) {
135+
crumb.text = docLibName;
136+
crumb.libraryData.title = docLibName;
137+
}
138+
});
139+
this.setState({ breadcrumbItems: updatedBCItems });
140+
}
141+
}).catch((err) => {
142+
console.log("[SiteFilePicker] Failed To Fetch defaultLibraryName, defaulting to internal name");
143+
});
144+
}
145+
39146
public render(): React.ReactElement<ISiteFilePickerTabProps> {
40147
return (
41-
<div className={styles.tabContainer}>
148+
<div className={styles.tabContainer} >
42149
<div className={styles.tabHeaderContainer}>
43-
<Breadcrumb items={this.state.breadcrumbItems} /*onRenderItem={this.renderBreadcrumbItem}*/ className={styles.breadcrumbNav}/>
150+
<Breadcrumb items={this.state.breadcrumbItems} /*onRenderItem={this.renderBreadcrumbItem}*/ className={styles.breadcrumbNav} />
44151
</div>
45152
<div className={styles.tabFiles}>
46153
{this.state.libraryAbsolutePath === undefined &&
@@ -142,7 +249,7 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
142249
/**
143250
* Triggered when user opens a file folder
144251
*/
145-
private _handleOpenFolder = (folder: IFile, addBreadcrumbNode: boolean) => {
252+
private _handleOpenFolder = (folder: IFolder, addBreadcrumbNode: boolean) => {
146253
const { breadcrumbItems } = this.state;
147254

148255
if (addBreadcrumbNode) {
@@ -151,9 +258,9 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
151258
folderData: folder,
152259
isCurrentItem: true,
153260
text: folder.name,
154-
key: folder.absoluteUrl
261+
key: folder.absoluteUrl,
262+
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
155263
};
156-
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); };
157264
breadcrumbItems.push(breadcrumbNode);
158265
}
159266

@@ -177,9 +284,9 @@ export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTa
177284
libraryData: library,
178285
isCurrentItem: true,
179286
text: library.title,
180-
key: library.serverRelativeUrl
287+
key: library.serverRelativeUrl,
288+
onClick: (ev, itm) => { this.onBreadcrumpItemClick(itm); }
181289
};
182-
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); };
183290
breadcrumbItems.push(breadcrumbNode);
184291
}
185292
this.setState({

src/services/FileBrowserService.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,30 @@ export class FileBrowserService {
8080
}
8181
}
8282

83+
/**
84+
* Gets document and media libraries from the site
85+
*/
86+
public getLibraryNameByInternalName = async (internalName: string): Promise<string> => {
87+
try {
88+
const absoluteUrl = this.context.pageContext.web.absoluteUrl;
89+
const restApi = `${absoluteUrl}/_api/web/GetFolderByServerRelativeUrl('${internalName}')/Properties?$select=vti_x005f_listtitle`;
90+
const libraryResult = await this.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1);
91+
92+
if (!libraryResult || !libraryResult.ok) {
93+
throw new Error(`Something went wrong when executing request. Status='${libraryResult.status}'`);
94+
}
95+
const libResults: { vti_x005f_listtitle: string } = await libraryResult.json();
96+
if (!libResults || !libResults.vti_x005f_listtitle) {
97+
throw new Error(`Cannot read data from the results.`);
98+
}
99+
100+
return libResults.vti_x005f_listtitle != internalName && libResults.vti_x005f_listtitle || "";
101+
} catch (error) {
102+
console.error(`[FileBrowserService.getSiteLibraryNameByInternalName]: Err='${error.message}'`);
103+
return null;
104+
}
105+
}
106+
83107
/**
84108
* Downloads document content from SP location.
85109
*/
@@ -118,7 +142,7 @@ export class FileBrowserService {
118142
}
119143
};
120144
if (folderPath) {
121-
body.parameters["FolderServerRelativeUrl"] = folderPath;
145+
body.parameters["FolderServerRelativeUrl"] = folderPath;
122146
}
123147
const data: any = await this.context.spHttpClient.fetch(restApi, SPHttpClient.configurations.v1, {
124148
method: "POST",

src/services/FileBrowserService.types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export interface IFile {
2626
supportsThumbnail: boolean;
2727
}
2828

29+
export interface IFolder extends Pick<IFile, "name" | "absoluteUrl" | "serverRelativeUrl"> {
30+
31+
}
32+
2933
export interface ILibrary {
3034
title: string;
3135
absoluteUrl: string;

src/webparts/controlsTest/components/ControlsTest.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ import { SitePicker } from "../../../controls/sitePicker/SitePicker";
167167
import { DynamicForm } from '../../../controls/dynamicForm';
168168
import { LocationPicker } from "../../../controls/locationPicker/LocationPicker";
169169
import { ILocationPickerItem } from "../../../controls/locationPicker/ILocationPicker";
170+
import { debounce } from "lodash";
170171

171172
// Used to render document card
172173
/**
@@ -1210,14 +1211,12 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
12101211
iconName="Upload"
12111212
labelMessage="My custom upload File"
12121213
>
1213-
12141214
<Placeholder iconName='BulkUpload'
12151215
iconText='Drag files or folder with files here...'
12161216
description={defaultClassNames => <span className={defaultClassNames}>Drag files or folder with files here...</span>}
12171217
buttonLabel='Configure'
12181218
hideButton={this.props.displayMode === DisplayMode.Read}
12191219
onConfigure={this._onConfigure} />
1220-
12211220
</DragDropFiles>
12221221
<br></br>
12231222

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

13241323
<div className="ms-font-m">Site picker tester:
1325-
<SitePicker
1324+
<SitePicker
13261325
context={this.props.context}
13271326
label={'select sites'}
13281327
mode={'site'}
@@ -1531,11 +1530,16 @@ export default class ControlsTest extends React.Component<IControlsTestProps, IC
15311530

15321531
<div>
15331532
<h3>File Picker</h3>
1533+
<TextField
1534+
label="Default SiteFileTab Folder"
1535+
onChange={debounce((ev, newVal) => { this.setState({ filePickerDefaultFolderAbsolutePath: newVal }); }, 500)}
1536+
styles={{ root: { marginBottom: 10 } }}
1537+
/>
15341538
<FilePicker
15351539
bingAPIKey="<BING API KEY>"
1540+
defaultFolderAbsolutePath={this.state.filePickerDefaultFolderAbsolutePath}
15361541
//accepts={[".gif", ".jpg", ".jpeg", ".bmp", ".dib", ".tif", ".tiff", ".ico", ".png", ".jxr", ".svg"]}
15371542
buttonLabel="Add File"
1538-
15391543
buttonIconProps={{ iconName: 'Add', styles: { root: { fontSize: 42 } } }}
15401544
onSave={this._onFilePickerSave}
15411545
onChange={(filePickerResult: IFilePickerResult[]) => { console.log(filePickerResult); }}

src/webparts/controlsTest/components/IControlsTestProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ export interface IControlsTestState {
3535
showCustomisedAnimatedDialog?: boolean;
3636
showSuccessDialog?: boolean;
3737
showErrorDialog?: boolean;
38+
filePickerDefaultFolderAbsolutePath?: string;
3839
}

0 commit comments

Comments
 (0)