Skip to content

Commit c6df12e

Browse files
author
Piotr Siatka
committed
Add OneDrive files support.
Fix bugs.
1 parent 155c66c commit c6df12e

28 files changed

+379
-260
lines changed

src/controls/filePicker/FilePicker.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ import RecentFilesTab from './RecentFilesTab/RecentFilesTab';
2121
import OneDriveTab from './OneDriveTab/OneDriveTab';
2222

2323
import styles from './FilePicker.module.scss';
24+
import { FileBrowserService } from '../../services/FileBrowserService';
25+
import { OneDriveFilesTab } from './OneDriveFilesTab';
26+
import { OneDriveService } from '../../services/OneDriveService';
2427

2528

2629
export class FilePicker extends React.Component<IFilePickerProps, IFilePickerState> {
30+
private fileBrowserService: FileBrowserService;
31+
private oneDriveService: OneDriveService;
2732
constructor(props: IFilePickerProps) {
2833
super(props);
2934

35+
// Initialize file browser services
36+
this.fileBrowserService = new FileBrowserService(props.webPartContext);
37+
this.oneDriveService = new OneDriveService(props.webPartContext);
38+
3039
this.state = {
3140
panelOpen: false,
3241
selectedTab: 'keyRecent',
@@ -153,6 +162,7 @@ export class FilePicker extends React.Component<IFilePickerProps, IFilePickerSta
153162
{
154163
this.state.selectedTab === "keySite" &&
155164
<SiteFilePickerTab
165+
fileBrowserService={this.fileBrowserService}
156166
context={this.props.webPartContext}
157167
accepts={accepts}
158168
onClose={() => this._handleClosePanel()}
@@ -170,7 +180,8 @@ export class FilePicker extends React.Component<IFilePickerProps, IFilePickerSta
170180
}
171181
{
172182
this.state.selectedTab === "keyOneDrive" &&
173-
<OneDriveTab
183+
<OneDriveFilesTab
184+
oneDriveService={this.oneDriveService}
174185
context={this.props.webPartContext}
175186
accepts={accepts}
176187
onClose={() => this._handleClosePanel()}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { IFilePickerTab } from "../FilePicker.types";
2+
import { OneDriveService } from "../../../services/OneDriveService";
3+
4+
export interface IOneDriveFilesTabProps extends IFilePickerTab {
5+
oneDriveService: OneDriveService;
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { OneDriveFilesBreadcrumbItem } from "./OneDriveFilesTab.types";
2+
3+
export interface IOneDriveFilesTabState {
4+
fileUrl?: string;
5+
libraryAbsolutePath: string;
6+
libraryTitle: string;
7+
folderPath: string;
8+
folderName: string;
9+
10+
breadcrumbItems: OneDriveFilesBreadcrumbItem[];
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "../FilePicker.module.scss";
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import * as React from 'react';
2+
import { IOneDriveFilesTabProps, IOneDriveFilesTabState } from '.';
3+
import { IFile } from '../../../services/FileBrowserService.types';
4+
import { OneDriveFilesBreadcrumbItem } from './OneDriveFilesTab.types';
5+
import { findIndex } from '@microsoft/sp-lodash-subset';
6+
7+
8+
import { Breadcrumb } from 'office-ui-fabric-react/lib/Breadcrumb';
9+
import { FileBrowser } from '../controls';
10+
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
11+
12+
import styles from './OneDriveFilesTab.module.scss';
13+
import * as strings from 'ControlStrings';
14+
15+
export class OneDriveFilesTab extends React.Component<IOneDriveFilesTabProps, IOneDriveFilesTabState> {
16+
constructor(props: IOneDriveFilesTabProps) {
17+
super(props);
18+
19+
this.state = {
20+
libraryAbsolutePath: undefined,
21+
libraryTitle: strings.DocumentLibraries,
22+
folderPath: undefined,
23+
folderName: strings.DocumentLibraries,
24+
breadcrumbItems: []
25+
};
26+
}
27+
28+
public async componentDidMount() {
29+
const folderPath = await this.props.oneDriveService.getOneDriveRootFolderRelativeUrl();
30+
const libraryAbsolutePath = await this.props.oneDriveService.getOneDriveRootFolderFullUrl();
31+
const libraryTitle = await this.props.oneDriveService.getOneDrivePersonalLibraryTitle();
32+
33+
const oneDriveFolderData: IFile = {
34+
isFolder: true,
35+
modified: null,
36+
absoluteRef: libraryAbsolutePath,
37+
fileLeafRef: libraryTitle,
38+
docIcon: "",
39+
fileRef: "",
40+
}
41+
42+
const breadcrumbItems = this.state.breadcrumbItems;
43+
// Add OneDrive folder as a first node
44+
const breadcrumbNode: OneDriveFilesBreadcrumbItem = {
45+
folderData: oneDriveFolderData,
46+
isCurrentItem: true,
47+
text: oneDriveFolderData.fileLeafRef,
48+
key: oneDriveFolderData.absoluteRef
49+
};
50+
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); }
51+
breadcrumbItems.push(breadcrumbNode);
52+
53+
this.setState({
54+
libraryAbsolutePath: libraryAbsolutePath,
55+
folderName: folderPath,
56+
libraryTitle
57+
})
58+
}
59+
60+
public render(): React.ReactElement<IOneDriveFilesTabProps> {
61+
return (
62+
<div className={styles.tabContainer}>
63+
<div className={styles.tabHeaderContainer}>
64+
{ /** TODO: Fix breadcrumb styles */}
65+
<Breadcrumb items={this.state.breadcrumbItems} className={styles.tabHeader}/>
66+
</div>
67+
<div className={styles.tab}>
68+
69+
{this.state.libraryAbsolutePath !== undefined &&
70+
<FileBrowser
71+
onChange={(fileUrl: string) => this._handleSelectionChange(fileUrl)}
72+
onOpenFolder={(folder: IFile) => this._handleOpenFolder(folder, true)}
73+
fileBrowserService={this.props.oneDriveService}
74+
libraryName={this.state.libraryTitle}
75+
folderPath={this.state.folderPath}
76+
accepts={this.props.accepts} />}
77+
</div>
78+
<div className={styles.actionButtonsContainer}>
79+
<div className={styles.actionButtons}>
80+
<PrimaryButton
81+
disabled={!this.state.fileUrl}
82+
onClick={() => this._handleSave()} className={styles.actionButton}>{strings.OpenButtonLabel}</PrimaryButton>
83+
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
84+
</div>
85+
</div>
86+
</div>
87+
);
88+
}
89+
90+
/**
91+
* Handles breadcrump item click
92+
*/
93+
private onBreadcrumpItemClick = (node: OneDriveFilesBreadcrumbItem) => {
94+
let { breadcrumbItems } = this.state;
95+
let breadcrumbClickedItemIndx = 0;
96+
// Site node clicked
97+
if (node.folderData == null) {
98+
this.setState({
99+
libraryAbsolutePath: undefined,
100+
folderPath: undefined,
101+
folderName: undefined
102+
});
103+
}
104+
// Check if it is folder item
105+
else if (node.folderData != null) {
106+
this._handleOpenFolder(node.folderData, false);
107+
// select which node has been clicked
108+
breadcrumbClickedItemIndx = findIndex(breadcrumbItems, item => item.folderData && item.folderData.absoluteRef === node.key);
109+
}
110+
111+
// Trim nodes array
112+
breadcrumbItems = breadcrumbItems.slice(0, breadcrumbClickedItemIndx + 1);
113+
// Set new current node
114+
breadcrumbItems[breadcrumbItems.length - 1].isCurrentItem = true;
115+
116+
this.setState({
117+
breadcrumbItems,
118+
fileUrl: undefined
119+
});
120+
}
121+
122+
/**
123+
* Is called when user selects a different file
124+
*/
125+
private _handleSelectionChange = (imageUrl: string) => {
126+
this.setState({
127+
fileUrl: imageUrl
128+
});
129+
}
130+
131+
/**
132+
* Called when user saves
133+
*/
134+
private _handleSave = () => {
135+
this.props.onSave(encodeURI(this.state.fileUrl));
136+
}
137+
138+
/**
139+
* Called when user closes tab
140+
*/
141+
private _handleClose = () => {
142+
this.props.onClose();
143+
}
144+
145+
/**
146+
* Triggered when user opens a file folder
147+
*/
148+
private _handleOpenFolder = (folder: IFile, addBreadcrumbNode: boolean) => {
149+
const { breadcrumbItems } = this.state;
150+
151+
if (addBreadcrumbNode) {
152+
breadcrumbItems.map(item => item.isCurrentItem = false);
153+
const breadcrumbNode: OneDriveFilesBreadcrumbItem = {
154+
folderData: folder,
155+
isCurrentItem: true,
156+
text: folder.fileLeafRef,
157+
key: folder.absoluteRef
158+
};
159+
breadcrumbNode.onClick = () => { this.onBreadcrumpItemClick(breadcrumbNode); }
160+
breadcrumbItems.push(breadcrumbNode);
161+
}
162+
163+
this.setState({
164+
folderPath: folder.fileRef,
165+
folderName: folder.fileLeafRef,
166+
libraryAbsolutePath: folder.absoluteRef,
167+
breadcrumbItems
168+
});
169+
}
170+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { IBreadcrumbItem } from "office-ui-fabric-react/lib/components/Breadcrumb";
2+
import { IFile } from "../../../services/FileBrowserService.types";
3+
4+
export interface OneDriveFilesBreadcrumbItem extends IBreadcrumbItem {
5+
folderData?: IFile;
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './OneDriveFilesTab';
2+
export * from './IOneDriveFilesTabProps';
3+
export * from './IOneDriveFilesTabState';

src/controls/filePicker/OneDriveTab/OneDriveTab.tsx

Lines changed: 2 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export default class OneDriveTab extends React.Component<IOneDriveTabProps, IOne
265265
*/
266266
public componentDidMount(): void {
267267
// Initialize the OneDrive services
268-
this._oneDriveService = new OneDriveService(this.props.context, this.props.accepts);
268+
// this._oneDriveService = new OneDriveService(this.props.context, this.props.accepts);
269269

270270
// Get the items at the root of the OneDrive folder
271271
this._getListItems();
@@ -284,85 +284,8 @@ export default class OneDriveTab extends React.Component<IOneDriveTabProps, IOne
284284
/**
285285
* Gets the list of items to display
286286
*/
287-
private _getListItems = (): Promise<void> => {
288-
// We're loading!
289-
this.setState({
290-
isLoading: true
291-
});
292-
293-
return this._oneDriveService.getListDataAsStream(this.state.serverRelativeFolderUrl).then((listDataStream: IGetListDataAsStreamResult) => {
294-
295-
// Get the thumbnail URL template -- stored in the list schema
296-
const thumbnailUrlTemplate: string = listDataStream.ListSchema[".thumbnailUrl"]
297-
.replace("{.mediaBaseUrl}", listDataStream.ListSchema[".mediaBaseUrl"])
298-
.replace("{.callerStack}", listDataStream.ListSchema[".callerStack"])
299-
.replace("{.driveAccessToken}", "encodeFailures=1&ctag={.ctag}");
300-
301-
// Map every item to a OneDrive file
302-
const files: IOneDriveFile[] = listDataStream.ListData.Row.map((item: IRow) => {
303-
// Build the thumbnail URL from the template
304-
// The template is stored in the schema (see above) and contains list-specific
305-
// replacement tokens (which we already replaced above) and item-specific
306-
// tokens, which we're replacing right. now.
307-
const thumbnail: string = thumbnailUrlTemplate
308-
.replace('{.spItemUrl}', item[".spItemUrl"])
309-
.replace('{.ctag}', encodeURIComponent(item[".ctag"]))
310-
.replace('{.fileType}', item[".fileType"]);
311-
312-
// Get the modified date
313-
const modifiedParts: string[] = item["Modified.FriendlyDisplay"]!.split('|');
314-
let modified: string = item.Modified;
315-
316-
// If there is a friendly modified date, use that
317-
// The friendly dates seem to be a lot smarter than what I have here.
318-
// For example, it seems to use a different structure for dates that are on the same
319-
// day, within a few days, etc.
320-
// For this example, we just handle the regular friendly-dates, but if we
321-
// turn this into a PnP control, we'll want to handle all sorts of friendly dates
322-
if (modifiedParts.length === 2) {
323-
modified = modifiedParts[1];
324-
}
325-
326-
// Parse media metadata to see if we can get known dimensions
327-
// Dimensions are stored as HTML-encoded JSON from media services.
328-
// If it is available, get the JSON structure and parse it.
329-
const media: any = item.MediaServiceFastMetadata && JSON.parse(unescape(item.MediaServiceFastMetadata));
330-
const dimensions: IDimensions = media && media.photo && {
331-
width: media.photo.width,
332-
height: media.photo.height
333-
};
334-
335-
// Create a nice OneDriveFile interface so we're not saving all that extra metadata that
336-
// gets returned from SharePoint in our state.
337-
const file: IOneDriveFile = {
338-
key: item.UniqueId,
339-
name: item.FileLeafRef,
340-
absoluteUrl: this._buildOneDriveAbsoluteUrl(listDataStream.HttpRoot, item.FileRef),
341-
serverRelativeUrl: item.FileRef,
342-
isFolder: item.FSObjType === "1",
343-
modified: modified,
344-
modifiedBy: item.Editor[0].title,
345-
fileType: item.File_x0020_Type,
346-
fileIcon: item["HTML_x0020_File_x0020_Type.File_x0020_Type.mapico"],
347-
fileSizeDisplay: item.FileSizeDisplay,
348-
totalFileCount: +item.SMTotalFileCount, // quickly converts string to number
349-
thumbnail: thumbnail,
350-
dimensions: dimensions,
351-
isShared: parseInt(item.PrincipalCount) > 0
352-
};
353-
return file;
354-
});
355-
356-
// Set the selection items so that we know what item we're selecting
357-
this._selection.setItems(files);
287+
private _getListItems = () => {
358288

359-
// Store the files and stop the loading icon
360-
this.setState({
361-
files: files,
362-
isLoading: false, // we're done loading
363-
parentFolderInfo: listDataStream.ParentInfo.ParentFolderInfo // remember where we are
364-
});
365-
});
366289
}
367290

368291
/**

src/controls/filePicker/SiteFilePickerTab/FileBrowser/FileBrowser.module.scss

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import { IFilePickerTab } from "..";
2+
import { FileBrowserService } from "../../../services/FileBrowserService";
23

3-
export interface ISiteFilePickerTabProps extends IFilePickerTab { }
4+
export interface ISiteFilePickerTabProps extends IFilePickerTab {
5+
fileBrowserService: FileBrowserService;
6+
}

0 commit comments

Comments
 (0)