React Comparer Web Part (#768)
* Initial version * Added more property pane fields * Fixed recent tab style * Update RecentFilesTab.module.scss * Update RecentFilesTab.tsx * Fixed an issue with unitialized PnP causing wrong API url * Update package-solution.json * Cleaned up code and comments. * Update FileBrowser.tsx * Update FileBrowser.types.ts * Update DocumentLibraryBrowser.tsx * Update RecentFilesTab.tsx * Added OneDrive support * Fixed a little speelliing mistake
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.7.1",
|
||||
"libraryName": "react-comparer",
|
||||
"libraryId": "8bc49dd2-60c6-46dc-989b-43a53f055bd0",
|
||||
"packageManager": "npm",
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
# react-comparer
|
||||
|
||||
![The web part in action](./assets/ComparerWebPart.gif)
|
||||
|
||||
## Summary
|
||||
|
||||
Allows users to compare Before and After pictures, with a draggable slider. Implements a custom file picker.
|
||||
|
||||
## Background
|
||||
|
||||
The Microsoft UI Fabric team (formerly Office UI Fabric) recently released a [web site](https://fluentfabric.azurewebsites.net/#/components) that allows you to compare the current version of the Office UI Fabric components side by side with their upcoming Fluent versions, using a cool image slider.
|
||||
|
||||
![Microsoft Ui Fabric slider in action](./assets/FluentSlider.gif)
|
||||
|
||||
I wanted to re-create a version of the slider that would allow a user to compare two images, side by side.
|
||||
|
||||
Re-creating the slider was pretty easy (they use [react-draggable](https://github.com/mzabriskie/react-draggable) to handle dragging); I wanted to make it possible for users to select the
|
||||
Before and After pictures to use the SharePoint File Picker dialog.
|
||||
|
||||
After a lot of digging around, I decided to re-create my own.
|
||||
|
||||
After all, one of the premises of SPFx is that SPFx gives us -- the SharePoint developer community -- a framework to build the same web parts that Microsoft's own developers do. Right?
|
||||
|
||||
This sample re-creates the SharePoint file picker (or, more accurately, the SharePoint image picker) that is used by out-of-the-box web parts such as the Image and Hero web parts. My goal was to create something that would be re-usable,
|
||||
and would be as close as possible from the out-of-the-box picker. In fact, as I was testing the app, I found myself testing the out-of-the-box picker when I thought I was testing my own file picker.
|
||||
|
||||
After building this sample, I can confirm that it _is_ possible to build web parts that look and feel like the out-of-the-box web parts using SPFx.
|
||||
|
||||
The file picker includes the following tabs:
|
||||
|
||||
### Recent
|
||||
|
||||
![Recent tab](./assets/RecentTab.gif)
|
||||
|
||||
### Web search
|
||||
|
||||
![Web search tab](./assets/WebSearchTab.gif)
|
||||
> NOTE: Requires a Bing API key, see below
|
||||
|
||||
### OneDrive
|
||||
|
||||
![OneDrive tab](./assets/OneDriveTab.gif)
|
||||
|
||||
### Site
|
||||
|
||||
![Site tab](./assets/SitesTab.gif)
|
||||
|
||||
### Upload
|
||||
|
||||
![Upload tab](./assets/UploadTab.gif)
|
||||
|
||||
### From a link
|
||||
|
||||
![From a link tab](./assets/FromLinkTab.gif)
|
||||
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![SPFx v1.7.1](https://img.shields.io/badge/SPFx-1.7.1-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-comparer | Hugo Bernier ([Tahoe Ninjas](http://tahoeninjas.blog), @bernierh)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|January 27, 2019|Initial release
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
* Clone this repository
|
||||
* In the command line run:
|
||||
* `npm i`
|
||||
* `gulp serve --nobrowser`
|
||||
* In the web browser navigate to the hosted version of the SharePoint workbench located in the same site as where the Tasks list is, eg. *https://contoso.sharepoint.com/sites/team/_layouts/15/workbench.aspx*.
|
||||
|
||||
## Optional Configuration - Bing Search
|
||||
|
||||
The sample `PropertyPanelFilePicker` is designed to mimic the standard SharePoint file picker dialog. As such, it includes a **Web search** which uses Bing to return search results.
|
||||
|
||||
![Web search with a Bing API key](./assets/WebSearch.gif)
|
||||
|
||||
However, the feature requires a Bing API key in order to work. If you do not have a Bing API key, the `PropertyPanelFilePicker` will display a message indicating that the Bing API key is missing.
|
||||
|
||||
![Web search without a Bing API key](./assets/WebSearchNoAPI.png)
|
||||
|
||||
### To Configure a Bing API key (Optional)
|
||||
|
||||
This sample uses **SharePoint Online Tenant Properties** to store the API key. The idea is that if you get an
|
||||
API key for your organization to use, you want to store it in one place -- the **Tenant Properties** -- instead of having to re-enter the API key
|
||||
in every web part that needs it.
|
||||
|
||||
To configure your API key, use the following steps:
|
||||
|
||||
1. If you don't already have a Bing Web Search API key, go to the [Bing Web Search](https://azure.microsoft.com/en-us/services/cognitive-services/bing-web-search-api/) API page and request a key.
|
||||
2. To set the API key, you'll need to use the **Office 365 CLI**. If you haven't installed it yet, you can get it [from here](https://pnp.github.io/office365-cli/?utm_campaign=Use+SharePoint+Online+tenant+properties&utm_medium=page&utm_source=msft_docs).
|
||||
3. Using **Office 365 CLI**, enter the following command, making sure to change **value** with your actual Bing API key, and **appCatalogUrl** with your tenant's app catalog (e.g.: https://contoso.sharepoint.com/sites/apps).
|
||||
|
||||
```PowerShell
|
||||
spo storageentity set --appCatalogUrl <appCatalogUrl> --key BingApi --value <value>
|
||||
```
|
||||
|
||||
4. If you want to verify that your API key is stored, use the following command to list all your tenant properties:
|
||||
|
||||
```PowerShell
|
||||
spo storageentity list --appCatalogUrl <appCatalogUrl>
|
||||
```
|
||||
|
||||
If all goes well, you should see an entry called **BingApi** containing your Bing API key.
|
||||
|
||||
### To hide the Web search tab
|
||||
|
||||
If you want to use this web part without the **Web search** tab, you can simply disable it from the code. To do so, use the following steps:
|
||||
|
||||
1. In the solution, open the **ComparerWebPart.ts** file.
|
||||
2. Look for a line at the top of the file that looks like this:
|
||||
|
||||
```TypeScript
|
||||
const DISABLE_WEB_SEARCH_TAB: boolean = false;
|
||||
```
|
||||
|
||||
3. Change the `false` to `true`
|
||||
4. Save and rebuild/redeploy the web part.
|
||||
|
||||
This constant will be applied to both `PropertyPaneFilePicker` controls used in the web part.
|
||||
|
||||
You can also simply set each `PropertyPaneFilePicker`'s `disableWebSearchTab` property to `true`.
|
||||
|
||||
## Features
|
||||
|
||||
* Using Tenant property
|
||||
* Using of Office UI Fabric React Selection control
|
||||
* Using of Office UI Fabric React Selection control
|
||||
* Making CORS calls within an SPFx component
|
||||
|
||||
### Resources
|
||||
|
||||
* [Fluent for Fabric](https://fluentfabric.azurewebsites.net/#/components)
|
||||
* [Build custom controls for the property pane](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/build-custom-property-pane-controls)
|
||||
* [Office UI Fabric](https://dev.office.com/fabric)
|
||||
* [Button](https://developer.microsoft.com/en-us/fabric#/components/button)
|
||||
* [CommandBar](https://developer.microsoft.com/en-us/fabric#/components/commandbar)
|
||||
* [DetailsList](https://developer.microsoft.com/en-us/fabric#/components/detailslist)
|
||||
* [FocusZone](https://developer.microsoft.com/en-us/fabric#/components/focuszone)
|
||||
* [Label](https://developer.microsoft.com/en-us/fabric#/components/label)
|
||||
* [Link](https://developer.microsoft.com/en-us/fabric#/components/link)
|
||||
* [List](https://developer.microsoft.com/en-us/fabric#/components/list)
|
||||
* [MessageBar](https://developer.microsoft.com/en-us/fabric#/components/messagebar)
|
||||
* [Nav](https://developer.microsoft.com/en-us/fabric#/components/nav)
|
||||
* [Panel](https://developer.microsoft.com/en-us/fabric#/components/panel)
|
||||
* [SearchBox](https://developer.microsoft.com/en-us/fabric#/components/searchbox)
|
||||
* [Selection](https://developer.microsoft.com/en-us/fabric#/components/selection)
|
||||
* [Spinner](https://developer.microsoft.com/en-us/fabric#/components/spinner)
|
||||
* [TextField](https://developer.microsoft.com/en-us/fabric#/components/textfield)
|
||||
* [SharePoint Online tenant properties](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/tenant-properties)
|
||||
* [@pnp/spfx-controls-react](https://sharepoint.github.io/sp-dev-fx-controls-react/)
|
||||
* [Placeholder control](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/Placeholder/)
|
||||
* [WebPartTitle](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/WebPartTitle/)
|
||||
* [@pnp/sp](https://pnp.github.io/pnpjs/sp/docs/)
|
||||
* [React-draggable](https://github.com/mzabriskie/react-draggable)
|
||||
* [React-block-image](https://github.com/transitive-bullshit/react-block-image)
|
||||
* [StackExchange - SPFx calls blocked by CORS policy](https://sharepoint.stackexchange.com/questions/254050/spfx-calls-blocked-by-cors-policy)
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-comparer" />
|
After Width: | Height: | Size: 4.6 MiB |
After Width: | Height: | Size: 192 KiB |
After Width: | Height: | Size: 507 KiB |
After Width: | Height: | Size: 10 MiB |
After Width: | Height: | Size: 5.5 MiB |
After Width: | Height: | Size: 698 KiB |
After Width: | Height: | Size: 867 KiB |
After Width: | Height: | Size: 19 MiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 4.9 MiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"comparer-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/comparer/ComparerWebPart.js",
|
||||
"manifest": "./src/webparts/comparer/ComparerWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"ComparerWebPartStrings": "lib/webparts/comparer/loc/{locale}.js",
|
||||
"PropertyPaneFilePickerStrings": "lib/controls/propertypanefilepicker/loc/{locale}.js",
|
||||
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-comparer",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "Comparer",
|
||||
"id": "8bc49dd2-60c6-46dc-989b-43a53f055bd0",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/comparer.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "react-comparer",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.7.1",
|
||||
"@microsoft/sp-lodash-subset": "1.7.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.7.1",
|
||||
"@microsoft/sp-webpart-base": "1.7.1",
|
||||
"@pnp/common": "^1.2.8",
|
||||
"@pnp/logging": "^1.2.8",
|
||||
"@pnp/odata": "^1.2.8",
|
||||
"@pnp/sp": "^1.2.8",
|
||||
"@pnp/spfx-controls-react": "^1.11.0",
|
||||
"@pnp/spfx-property-controls": "^1.13.1",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.4.2",
|
||||
"@types/react-dom": "16.0.5",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"react": "16.3.2",
|
||||
"react-block-image": "^1.0.0",
|
||||
"react-dom": "16.3.2",
|
||||
"react-draggable": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.7.1",
|
||||
"@microsoft/sp-tslint-rules": "1.7.1",
|
||||
"@microsoft/sp-module-interfaces": "1.7.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.7.1",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
/**
|
||||
* Formats a file size in the right unit
|
||||
* TODO: Move to a common library
|
||||
*/
|
||||
export function FormatBytes(bytes, decimals) {
|
||||
if (bytes == 0) {
|
||||
return strings.EmptyFileSize;
|
||||
}
|
||||
|
||||
const k: number = 1024;
|
||||
const dm = decimals <= 0 ? 0 : decimals || 2;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + strings.SizeUnit[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current domain url
|
||||
*/
|
||||
export function GetAbsoluteDomainUrl(url: string): string {
|
||||
if (url !== undefined) {
|
||||
const myURL = new URL(url.toLowerCase());
|
||||
return myURL.protocol + "//" + myURL.host;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
# PropertyPaneFilePicker
|
||||
|
||||
Mimics the out-the-box SharePoint file picker, but provides customizable features.
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased/TODO]
|
||||
|
||||
- Improve support for files (vs. images)
|
||||
- OneDrive lists should have a chevron on headers
|
||||
- Site file picker: list browsing should support tile view.
|
||||
- Little sev 3 bug when selecting folders in OneDrive where it quickly marks the file in the same
|
||||
ordinal position as the folder as selected before folder is loaded.
|
||||
- Test for mobile devices (and fix issues)
|
||||
- Test for accessibility
|
||||
- Fix other inconsistencies
|
||||
|
||||
## [1.0.0] - 2019-01-27
|
||||
|
||||
### Added
|
||||
|
||||
- PropertyPaneFilePicker: control to allow browsing from a property pane
|
||||
- LinkFilePickerTab: allows users to enter a file by URL. Option to allow only files within same domain.
|
||||
- RecentFilesTab: allows users to select a file from their recent files in SharePoint.
|
||||
- SiteFilePickerTab: allows users to find files from within the current site.
|
||||
- UploadFilePickerTab: allows users to upload a file.
|
||||
- WebSearch: allows users to search Bing for files.
|
||||
- OneDriveTab: allows users to select files from their OneDrive.
|
||||
- Tile doesn't show hover border in recent files
|
||||
- Add ability to enter bing API and store in tenant store
|
||||
- Add ratio selector
|
||||
|
||||
### Changed
|
||||
|
||||
- No changes
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing deprecated
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing removed
|
||||
|
||||
### Fixed
|
||||
|
||||
- Nothing fixed
|
||||
|
||||
### Security
|
||||
|
||||
- No vulnerabilities fixed
|
|
@ -0,0 +1,10 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { ItemType } from "./IPropertyPaneFilePicker";
|
||||
|
||||
export interface IFilePickerTab {
|
||||
itemType: ItemType;
|
||||
context: WebPartContext;
|
||||
accepts: string;
|
||||
onSave: (value: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { IPropertyPaneCustomFieldProps, WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
|
||||
export interface IPropertyPaneFilePickerProps {
|
||||
key: string;
|
||||
label?: string;
|
||||
buttonLabel: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
webPartContext: WebPartContext;
|
||||
disableLocalUpload?: boolean;
|
||||
disableWebSearchTab?: boolean;
|
||||
disableCentralAssetRepo?: boolean; // not supported yet
|
||||
hasMySiteTab?: boolean;
|
||||
accepts?: string;
|
||||
itemType: ItemType;
|
||||
required?: boolean;
|
||||
onSave:(value:string)=>void;
|
||||
}
|
||||
|
||||
export interface IPropertyPaneFilePickerPropsInternal extends IPropertyPaneCustomFieldProps, IPropertyPaneFilePickerProps {}
|
||||
|
||||
export enum ItemType {
|
||||
Documents,
|
||||
Images
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { IPropertyPaneFilePickerProps } from '.';
|
||||
|
||||
export interface IPropertyPaneFilePickerHostProps extends IPropertyPaneFilePickerProps {
|
||||
onChanged: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface IPropertyPaneFilePickerHostState {
|
||||
showFullNav: boolean; // reserved for future use
|
||||
panelOpen: boolean;
|
||||
selectedTab: string;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.linkTextField {
|
||||
max-width: 640px;
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Custom styles
|
||||
import styles from './LinkFilePickerTab.module.scss';
|
||||
|
||||
// Custom props and state
|
||||
import { ILinkFilePickerTabProps, ILinkFilePickerTabState } from '.';
|
||||
import { ItemType } from '../IPropertyPaneFilePicker';
|
||||
|
||||
// Office Fabric
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
|
||||
// PnP
|
||||
import { FetchClient } from "@pnp/common";
|
||||
|
||||
// Localized strings
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
import { GetAbsoluteDomainUrl } from '../../../CommonUtils';
|
||||
|
||||
export default class LinkFilePickerTab extends React.Component<ILinkFilePickerTabProps, ILinkFilePickerTabState> {
|
||||
constructor(props: ILinkFilePickerTabProps) {
|
||||
super(props);
|
||||
this.state = { isValid: false };
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<ILinkFilePickerTabProps> {
|
||||
const imageType: boolean = this.props.itemType === ItemType.Images;
|
||||
return (
|
||||
<div className={styles.tabContainer}>
|
||||
<div className={styles.tabHeaderContainer}>
|
||||
<h2 className={styles.tabHeader}>{strings.LinkHeader}</h2>
|
||||
</div>
|
||||
<div className={styles.tab}>
|
||||
<TextField
|
||||
multiline={true}
|
||||
required={true}
|
||||
resizable={false}
|
||||
deferredValidationTime={300}
|
||||
className={styles.linkTextField}
|
||||
label={imageType ? strings.LinkImageInstructions : strings.LinkFileInstructions}
|
||||
ariaLabel={imageType ? strings.LinkImageInstructions : strings.LinkFileInstructions}
|
||||
defaultValue={"https://"}
|
||||
onGetErrorMessage={(value: string) => this._getErrorMessagePromise(value)}
|
||||
autoAdjustHeight={false}
|
||||
underlined={false}
|
||||
borderless={false}
|
||||
validateOnFocusIn={false}
|
||||
validateOnFocusOut={false}
|
||||
validateOnLoad={true}
|
||||
value={this.state.fileUrl}
|
||||
onChanged={(_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => this._handleChange(newValue)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
<div className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={!this.state.isValid}
|
||||
onClick={() => this._handleSave()} className={styles.actionButton}>{strings.OpenButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called as user types in a new value
|
||||
*/
|
||||
private _handleChange = (newValue?: string) => {
|
||||
this.setState({
|
||||
fileUrl: newValue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the url that was typed in
|
||||
* @param value
|
||||
*/
|
||||
private _getErrorMessagePromise(value: string): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
|
||||
// DOn't give an error for blank or placeholder value, but don't make it a valid entry either
|
||||
if (value === undefined || value === 'https://') {
|
||||
this.setState({ isValid: false });
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that user is typing a valid URL format
|
||||
if (!this._isUrl(value)) {
|
||||
this.setState({ isValid: false });
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't allow external links, verify that we're in the same domain
|
||||
if (!this.props.allowExternalTenantLinks && !this._isSameDomain(value)) {
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.NoExternalLinksValidationMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that item is an image
|
||||
if (this.props.itemType === ItemType.Images) {
|
||||
if (!this._isImage(value)) {
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.NoImageValidationMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the file exists by actually getting the item
|
||||
try {
|
||||
const client = new FetchClient();
|
||||
client.fetch(value, { method: "HEAD" }).then((response) => {
|
||||
if (!response.ok) {
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.CantValidateValidationMessage);
|
||||
return;
|
||||
}
|
||||
// the file exists
|
||||
this.setState({ isValid: true });
|
||||
resolve('');
|
||||
}, () => {
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.CantValidateValidationMessage);
|
||||
}).catch(() => {
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.CantValidateValidationMessage);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error verifying file", error);
|
||||
this.setState({ isValid: false });
|
||||
resolve(strings.CantValidateValidationMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when user saves
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* HAndles when user closes without saving
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a URL ?
|
||||
* (insert guy holding a butterfly meme)
|
||||
*/
|
||||
private _isUrl = (fileUrl: string): boolean => {
|
||||
try {
|
||||
const myURL = new URL(fileUrl.toLowerCase());
|
||||
return myURL.host !== undefined;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that file ends with an image extension.
|
||||
* Should really check the content type instead.
|
||||
*/
|
||||
private _isImage = (fileName: string): boolean => {
|
||||
const acceptableExtensions: string[] = this.props.accepts.toLowerCase().split(",");
|
||||
// ".gif,.jpg,.jpeg,.bmp,.dib,.tif,.tiff,.ico,.png,.jxr,.svg"
|
||||
|
||||
const thisExtension: string = this._getFileExtension(fileName);
|
||||
return acceptableExtensions.indexOf(thisExtension) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired from the code in PnP controls
|
||||
*/
|
||||
private _getFileExtension = (fileName): string => {
|
||||
|
||||
// Split the URL on the dots
|
||||
const splitFileName = fileName.toLowerCase().split('.');
|
||||
|
||||
// Take the last value
|
||||
let extensionValue = splitFileName.pop();
|
||||
|
||||
// Check if there are query string params in place
|
||||
if (extensionValue.indexOf('?') !== -1) {
|
||||
// Split the string on the question mark and return the first part
|
||||
const querySplit = extensionValue.split('?');
|
||||
extensionValue = querySplit[0];
|
||||
}
|
||||
|
||||
return `.${extensionValue}`;
|
||||
}
|
||||
|
||||
private _isSameDomain = (fileUrl: string): boolean => {
|
||||
const siteUrl: string = this.props.context.pageContext.web.absoluteUrl;
|
||||
return GetAbsoluteDomainUrl(siteUrl) === GetAbsoluteDomainUrl(fileUrl);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
|
||||
export interface ILinkFilePickerTabProps extends IFilePickerTab {
|
||||
allowExternalTenantLinks: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkFilePickerTabState {
|
||||
fileUrl?: string;
|
||||
isValid: boolean;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './LinkFilePickerTab';
|
||||
export * from './LinkFilePickerTab.types';
|
|
@ -0,0 +1,116 @@
|
|||
@import "../OneDriveTab.module.scss";
|
||||
|
||||
|
||||
.odItemTile2Image {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.odImageFrame2 {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.odImageFrame2Image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.odImageFrame {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.odImageStack {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.odImageStackTile {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
//opacity: 0;
|
||||
}
|
||||
|
||||
.odImageTile {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.odImageTile {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.odImageTileBackground {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.odImageTileImage {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.odImageTileImage, a .odImageTileImage {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.odImageTile img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.odItemTile2SmallIcon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.fileTypeIcon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.fileTypeIconIcon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import * as React from 'react';
|
||||
import styles from './DocumentTile.module.scss';
|
||||
import { IOneDriveFile } from '../OneDriveTab.types';
|
||||
import { css, IRenderFunction, IRectangle } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { Image, IImageProps, ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
import { Check } from 'office-ui-fabric-react/lib/Check';
|
||||
import { ScreenWidthMinLarge } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { IDimensions } from '../../../../services/OneDriveServices';
|
||||
|
||||
const MAX_ASPECT_RATIO = 3;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface IDocumentTileProps {
|
||||
item: IOneDriveFile;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
pageWidth: number;
|
||||
tileDimensions: IDimensions;
|
||||
onItemInvoked: (item: IOneDriveFile) => void;
|
||||
}
|
||||
|
||||
export interface IDocumentTileState { }
|
||||
|
||||
export class DocumentTile extends React.Component<IDocumentTileProps, IDocumentTileState> {
|
||||
public render(): React.ReactElement<IDocumentTileProps> {
|
||||
const { isSelected, index, item, pageWidth, tileDimensions } = this.props;
|
||||
const isLarge: boolean = pageWidth >= ScreenWidthMinLarge;
|
||||
|
||||
// Find the dimensions that are biggest
|
||||
let thumbnailWidth: number = tileDimensions.width;
|
||||
let thumbnailHeight: number = tileDimensions.height;
|
||||
|
||||
|
||||
if (item.dimensions) {
|
||||
const contentAspectRatio = item.dimensions.width / item.dimensions.height;
|
||||
const boundsAspectRatio = tileDimensions.width / tileDimensions.height;
|
||||
|
||||
let scale: number;
|
||||
|
||||
if (contentAspectRatio > boundsAspectRatio) {
|
||||
scale = tileDimensions.width / item.dimensions.width;
|
||||
} else {
|
||||
scale = tileDimensions.height / item.dimensions.height;
|
||||
}
|
||||
|
||||
const finalScale = Math.min(MAX_ASPECT_RATIO, scale);
|
||||
|
||||
thumbnailWidth = item.dimensions.width * finalScale;
|
||||
thumbnailHeight = item.dimensions.height * finalScale;
|
||||
}
|
||||
|
||||
const thumbnail: string = item.thumbnail + `&width=${thumbnailWidth}&height=${thumbnailHeight}`;
|
||||
const ariaLabel: string = strings.ImageAriaLabelTemplate.replace('{0}', item.fileIcon);
|
||||
const itemLabel: string = strings.DocumentLabelTemplate
|
||||
.replace('{0}', item.name)
|
||||
.replace('{1}', item.modified)
|
||||
.replace('{2}', item.modifiedBy);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-selected={isSelected}
|
||||
data-is-draggable={false}
|
||||
role="listitem"
|
||||
aria-labelledby={`Tile-label${index}`}
|
||||
aria-describedby={`Tile-activity${index}`}
|
||||
className={css(styles.tile, isLarge ? styles.isLarge : styles.isSmall, styles.invokable, styles.selectable, isSelected ? styles.selected : undefined)}
|
||||
data-is-focusable={true}
|
||||
data-is-sub-focuszone={true}
|
||||
data-disable-click-on-enter={true}
|
||||
data-selection-index={index}
|
||||
//data-selection-invoke={true}
|
||||
onClick={(_event)=>this.props.onItemInvoked(item)}
|
||||
>
|
||||
<div
|
||||
className={styles.link}
|
||||
role="link"
|
||||
//onClick={(_event) => this.props.onItemInvoked(item)}
|
||||
//data-selection-invoke={true}
|
||||
>
|
||||
<span
|
||||
id={`Tile-label${index}`}
|
||||
className={styles.label}>{itemLabel}</span>
|
||||
<span role="presentation" className={styles.aboveNameplate}>
|
||||
<span role="presentation" className={styles.content}>
|
||||
<span role="presentation" className={styles.foreground}>
|
||||
<span className={styles.odItemTile2Image}>
|
||||
<span className={styles.odImageFrame2} style={{ width: thumbnailWidth, height: thumbnailHeight }}>
|
||||
<span className={styles.odImageFrame2Image}>
|
||||
<span className={styles.odImageFrame}>
|
||||
<span className={styles.odImageStack}>
|
||||
<span className={styles.odImageStackTile}>
|
||||
<span className={styles.odImageTile}>
|
||||
<span className={styles.odImageTileBackground}>
|
||||
<Image
|
||||
src={thumbnail}
|
||||
width={thumbnailWidth}
|
||||
height={thumbnailHeight}
|
||||
imageFit={ImageFit.contain}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.odItemTile2SmallIcon} >
|
||||
<div className={styles.fileTypeIcon}
|
||||
aria-label={ariaLabel}
|
||||
title={ariaLabel}>
|
||||
<img className={styles.fileTypeIconIcon} src={strings.ODPhotoIconUrl} style={{ width: 16, height: 16 }} />
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.namePlate}>
|
||||
<span className={styles.name}>
|
||||
<span className={css(styles.signalField, styles.compact)}>
|
||||
<span className={styles.signalFieldValue}>{item.name}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.activity} id={`Tile-activity${index}`}>
|
||||
<span className={css(styles.signalField, styles.compact)}>
|
||||
<span className={styles.signalFieldValue}>{item.modified}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span role="checkbox" className={styles.check} data-selection-toggle={true} aria-checked={isSelected}>
|
||||
<Check checked={isSelected} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './DocumentTile';
|
|
@ -0,0 +1,106 @@
|
|||
@import "../OneDriveTab.module.scss";
|
||||
|
||||
.folderCover {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: end;
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.folderCover.isSmall_88d29bd4 {
|
||||
width: 72px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.folderCover.isLarge {
|
||||
width: 112px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.folderCoverBack {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -4px;
|
||||
|
||||
img {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.folderCoverFront {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -4px;
|
||||
|
||||
img {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.folderCoverFrame {
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
-webkit-box-shadow: 0 1px 3px 2px rgba(1, 1, 0, 0.2);
|
||||
box-shadow: 0 1px 3px 2px rgba(1, 1, 0, 0.2);
|
||||
background-color: $ms-color-white;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.folderCoverContent {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
top: 4px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import * as React from 'react';
|
||||
import styles from './FolderTile.module.scss';
|
||||
import { IOneDriveFile } from '../OneDriveTab.types';
|
||||
import { css, IRenderFunction, IRectangle } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { Icon, IconType } from 'office-ui-fabric-react/lib/Icon';
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
import { ScreenWidthMinLarge } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { IDimensions } from '../../../../services/OneDriveServices';
|
||||
|
||||
export interface IFolderTileProps {
|
||||
item: IOneDriveFile;
|
||||
index: number;
|
||||
isSelected: boolean;
|
||||
pageWidth: number;
|
||||
onItemInvoked: (item: IOneDriveFile) => void;
|
||||
tileDimensions: IDimensions;
|
||||
}
|
||||
|
||||
export interface IFolderTileState { }
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class FolderTile extends React.Component<IFolderTileProps, IFolderTileState> {
|
||||
public render(): React.ReactElement<IFolderTileProps> {
|
||||
const { isSelected, index, item, pageWidth } = this.props;
|
||||
const isLarge: boolean = pageWidth >= ScreenWidthMinLarge;
|
||||
//{item.name}, Folder, Modified {item.modified}, edited by {item.modifiedBy}, {item.totalFileCount} items, Private
|
||||
const itemLabel: string = strings.FolderLabelTemplate
|
||||
.replace('{0}', item.name)
|
||||
.replace('{1}', item.modified)
|
||||
.replace('{2}', item.modifiedBy)
|
||||
.replace('{3}', `${item.totalFileCount}`);
|
||||
return (
|
||||
<div
|
||||
aria-selected={isSelected}
|
||||
data-is-draggable={false}
|
||||
role="listitem"
|
||||
aria-labelledby={`Tile-label${index}`}
|
||||
aria-describedby={`Tile-activity${index}`}
|
||||
className={css(styles.tile, isLarge ? styles.isLarge : styles.isSmall, styles.invokable, isSelected ? styles.selected : undefined)}
|
||||
data-is-focusable={true}
|
||||
data-is-sub-focuszone={true}
|
||||
data-disable-click-on-enter={true}
|
||||
data-selection-index={index}
|
||||
onClick={(_event)=>this.props.onItemInvoked(item)}
|
||||
>
|
||||
<div
|
||||
className={styles.link}
|
||||
role="link"
|
||||
>
|
||||
<span
|
||||
id={`Tile-label${index}`}
|
||||
className={styles.label}>{itemLabel}</span>
|
||||
<span role="presentation" className={styles.aboveNameplate}>
|
||||
<span role="presentation" className={styles.content}>
|
||||
<span role="presentation" className={styles.foreground}>
|
||||
<span className={styles.odItemTile2FolderCover}>
|
||||
<div
|
||||
className={css(styles.folderCover, styles.isLarge)}>
|
||||
<Icon
|
||||
className={styles.folderCoverBack}
|
||||
iconType={IconType.image}
|
||||
imageProps={{
|
||||
src: strings.FolderBackPlate
|
||||
}} />
|
||||
{item.totalFileCount > 0 &&
|
||||
<span className={styles.folderCoverContent}>
|
||||
<span className={styles.folderCoverFrame}>
|
||||
<span className={styles.itemTileBlankCover} style={{ width: 104, height: 72 }}>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
<Icon
|
||||
className={styles.folderCoverFront}
|
||||
iconType={IconType.image}
|
||||
imageProps={{
|
||||
src: strings.FolderFrontPlate
|
||||
}} />
|
||||
{item.totalFileCount > 0 &&
|
||||
<span className={styles.metadata}>{item.totalFileCount}</span>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.namePlate}>
|
||||
<span className={styles.name}>
|
||||
<span className={css(styles.signalField, styles.compact)}>
|
||||
<span className={styles.signalFieldValue}>{item.name}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.activity} id={`Tile-activity${index}`}>
|
||||
<span className={css(styles.signalField, styles.compact)}>
|
||||
<span className={styles.signalFieldValue}>{item.modified}</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './FolderTile';
|
|
@ -0,0 +1,562 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.aboveNameplate {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.tileContent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.foreground {
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.grid {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
-webkit-box-align: start;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
-ms-flex-line-pack: start;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.cellContent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tile {
|
||||
outline: transparent;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
background-color: $ms-color-white;
|
||||
-webkit-transition: -webkit-transform 0.1s linear;
|
||||
transition: -webkit-transform 0.1s linear;
|
||||
transition: transform 0.1s linear;
|
||||
transition: transform 0.1s linear, -webkit-transform 0.1s linear;
|
||||
color: $ms-color-neutralPrimary;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
&.invokable {
|
||||
.foreground {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
-webkit-transform: scale(0.95);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.namePlate {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 20px;
|
||||
padding: 4px 8px 8px;
|
||||
background-color: $ms-color-white;
|
||||
color: $ms-color-neutralPrimary;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: .95;
|
||||
//border-top: 1px solid #eaeaea;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.nameplate .name {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .name {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .name {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.nameplate .name {
|
||||
-webkit-transition: color 0.2s linear;
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
color: $ms-color-neutralPrimary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.name,
|
||||
.isLarge .name {
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
margin-top: -7px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.tile:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .activity {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .activity {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.signalField,
|
||||
.signalField.compact {
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.signalField {
|
||||
max-width: 100%;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row nowrap;
|
||||
flex-direction: row nowrap;
|
||||
}
|
||||
|
||||
.signalField > span[class*="signalFieldValue"] {
|
||||
-ms-flex: 1 1 auto !important;
|
||||
flex: 1 1 auto !important;
|
||||
}
|
||||
|
||||
.signalFieldValue {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.activity {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #767676;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
height: 24px;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
.folderList :global(.ms-List-cell) {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
// .folderList :global(.ms-List-page) {
|
||||
// display: -webkit-box;
|
||||
// display: -ms-flexbox;
|
||||
// display: flex;
|
||||
// -webkit-box-orient: horizontal;
|
||||
// -webkit-box-direction: normal;
|
||||
// -ms-flex-direction: row;
|
||||
// flex-direction: row;
|
||||
// flex-wrap: wrap;
|
||||
// align-content: flex-start;
|
||||
// align-items: flex-start;
|
||||
// justify-content: flex-start;
|
||||
// margin-bottom: 36px;
|
||||
// margin-left: -4px;
|
||||
// margin-right: -4px;
|
||||
// margin-top: -4px;
|
||||
// }
|
||||
|
||||
.folderList :global(.ms-List-surface) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link {
|
||||
outline: transparent;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: stretch;
|
||||
-ms-flex-align: stretch;
|
||||
align-items: stretch;
|
||||
-ms-flex-line-pack: stretch;
|
||||
align-content: stretch;
|
||||
text-decoration: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding-top: 140px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listCell {
|
||||
// text-align: center;
|
||||
// outline: none;
|
||||
// position: relative;
|
||||
// max-width: 176px;
|
||||
// max-height: 171px;
|
||||
// margin: 4px;
|
||||
// border-style: none;
|
||||
// border-width: 0px;
|
||||
// width: 176px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-color: #ffffff;
|
||||
border-top-color: rgb(255, 255, 255);
|
||||
border-right-color: rgb(255, 255, 255);
|
||||
border-bottom-color: rgb(255, 255, 255);
|
||||
border-left-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
.foreground {
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
// .hasForegroundFrame .foreground {
|
||||
// background-color: #ffffff;
|
||||
// -webkit-box-shadow: 0 1px 3px 1px rgba(1, 1, 0, 0.05);
|
||||
// box-shadow: 0 1px 3px 1px rgba(1, 1, 0, 0.05);
|
||||
// min-width: 32px;
|
||||
// min-height: 32px;
|
||||
// }
|
||||
|
||||
// .hasForegroundFrame .foreground:after {
|
||||
// content: '';
|
||||
// display: block;
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// bottom: 0;
|
||||
// right: 0;
|
||||
// pointer-events: none;
|
||||
// border: 2px solid #ffffff;
|
||||
// }
|
||||
|
||||
.check {
|
||||
//display: none;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: end;
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
top: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
background-color: transparent;
|
||||
opacity: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
outline: transparent;
|
||||
position: absolute;
|
||||
|
||||
:global {
|
||||
.ms-Check .ms-Check-check,
|
||||
.ms-Check .ms-Check-circle {
|
||||
color: #797775;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #797775;
|
||||
}
|
||||
}
|
||||
|
||||
html[dir="ltr"] .check {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tile.selectable .check {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tile.selectable:hover {
|
||||
background-color: $ms-color-neutralLighter;
|
||||
}
|
||||
|
||||
.tile:hover .check,
|
||||
.tile.showCheck .check {
|
||||
opacity: 1;
|
||||
|
||||
:global {
|
||||
.ms-Check .ms-Check-check,
|
||||
.ms-Check .ms-Check-circle {
|
||||
color: $ms-color-neutralPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tile.selected .check {
|
||||
opacity: 1;
|
||||
:global {
|
||||
.ms-Check .ms-Check-check,
|
||||
.ms-Check .ms-Check-circle {
|
||||
color: $ms-color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialogMainOverride {
|
||||
:global {
|
||||
.ms-Dialog-title {
|
||||
color: $ms-color-themePrimary;
|
||||
}
|
||||
|
||||
.ms-Dialog-subText {
|
||||
font-size: 14px;
|
||||
color: $ms-color-neutralPrimary;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.itemTileBlankCover {
|
||||
background: $ms-color-white;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .metadata {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .metadata {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.isLarge .metadata {
|
||||
font-size: 14px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.metadata,
|
||||
.metadata.isSmall {
|
||||
font-size: 12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: block;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1 1;
|
||||
flex: 1 1;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listPage {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.odItemTile2FolderCover {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.standaloneListBreadcrumb {
|
||||
// font-size: 17px;
|
||||
// font-weight: 300;
|
||||
// }
|
||||
|
||||
// .xlg .standaloneListBreadcrumb, .xxlg .standaloneListBreadcrumb, .xxxlg .standaloneListBreadcrumb, .xxxxlg .standaloneListBreadcrumb {
|
||||
font-size: 21px;
|
||||
font-weight: 100;
|
||||
margin-bottom: 1px;
|
||||
padding-top:0;
|
||||
padding-bottom:0;
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
|
@ -0,0 +1,963 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Custom styles
|
||||
import styles from './OneDriveTab.module.scss';
|
||||
|
||||
// Office Fabric
|
||||
import { Dialog, DialogType, DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
|
||||
import { List, IPageProps } from 'office-ui-fabric-react/lib/List';
|
||||
import { css, IRenderFunction, IRectangle } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { SelectionZone } from 'office-ui-fabric-react/lib/Selection';
|
||||
import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb';
|
||||
import { CommandBar, ICommandBarItemProps } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
IColumn,
|
||||
IDetailsRowProps,
|
||||
DetailsRow
|
||||
} from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
|
||||
// Used to read JSON string as JSON
|
||||
import { unescape } from '@microsoft/sp-lodash-subset';
|
||||
|
||||
// Custom props and states
|
||||
import { IOneDriveTabProps, IOneDriveTabState, IOneDriveFile, ViewType } from './OneDriveTab.types';
|
||||
|
||||
// Localized resources
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
// Used to get OneDrive data
|
||||
import { IGetListDataAsStreamResult, IRow, IParentFolderInfo } from '../../../services/OneDriveServices/IGetListDataAsStreamResult';
|
||||
import { OneDriveServices, IDimensions } from '../../../services/OneDriveServices';
|
||||
|
||||
// Used to render custom tiles
|
||||
import { FolderTile } from './FolderTile/FolderTile';
|
||||
import { DocumentTile } from './DocumentTile/DocumentTile';
|
||||
import { FormatBytes, GetAbsoluteDomainUrl } from '../../../CommonUtils';
|
||||
|
||||
/**
|
||||
* Rows per page
|
||||
*/
|
||||
const ROWS_PER_PAGE: number = 3;
|
||||
|
||||
/**
|
||||
* Maximum row height
|
||||
*/
|
||||
const MAX_ROW_HEIGHT: number = 250;
|
||||
|
||||
/**
|
||||
* Maximum number of cells per page
|
||||
*/
|
||||
const CELLS_PER_PAGE: number = 100;
|
||||
|
||||
/**
|
||||
* Standard tile margin
|
||||
*/
|
||||
const STANDARD_TILE_MARGIN: number = 4;
|
||||
|
||||
/**
|
||||
* Standard left and right padding
|
||||
*/
|
||||
const TILE_HORZ_PADDING: number = 32;
|
||||
|
||||
/**
|
||||
* Standard bottom margin
|
||||
*/
|
||||
const BOTTOM_MARGIN: number = 36;
|
||||
|
||||
const LAYOUT_STORAGE_KEY: string = 'comparerOneDriveLayout';
|
||||
/**
|
||||
* This tab uses a different approach than the SiteFilePickerTab because,
|
||||
* unlike it, all requests are made to a separate site collection and separate domain.
|
||||
* Unfortuntely, we couldn't use the PnP libraries to make the API calls
|
||||
* and had to use RemoteWeb to allow retrieving content across domains.
|
||||
*
|
||||
*/
|
||||
export default class OneDriveTab extends React.Component<IOneDriveTabProps, IOneDriveTabState> {
|
||||
private _columnCount: number;
|
||||
private _columnWidth: number;
|
||||
private _rowHeight: number;
|
||||
private _selection: Selection;
|
||||
private _listElem: List = undefined;
|
||||
private _pageWidth: number;
|
||||
private _oneDriveService: OneDriveServices = undefined;
|
||||
|
||||
|
||||
constructor(props: IOneDriveTabProps) {
|
||||
super(props);
|
||||
|
||||
// If possible, load the user's favourite layout
|
||||
const lastLayout: ViewType = localStorage ?
|
||||
localStorage.getItem(LAYOUT_STORAGE_KEY) as ViewType
|
||||
: 'tiles' as ViewType;
|
||||
|
||||
// Set the columns we'll use for the list views
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: 'colIcon',
|
||||
name: 'Type',
|
||||
ariaLabel: strings.TypeAriaLabel,
|
||||
iconName: 'Page',
|
||||
isIconOnly: true,
|
||||
fieldName: 'docIcon',
|
||||
headerClassName: styles.iconColumnHeader,
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
// Get the icon
|
||||
const folderIcon: string = strings.FolderIconUrl;
|
||||
const iconUrl: string = strings.PhotoIconUrl;
|
||||
|
||||
// Insert file type into localized string
|
||||
const altText: string = item.isFolder ? strings.FolderAltText : strings.ImageAltText.replace('{0}', item.fileType);
|
||||
|
||||
//TODO: This will have to change if we add support for non-image fiels
|
||||
return <div className={styles.fileTypeIcon}>
|
||||
<img src={item.isFolder ? folderIcon : iconUrl} className={styles.fileTypeIconIcon} alt={altText} title={altText} />
|
||||
</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'colName',
|
||||
name: strings.NameField,
|
||||
fieldName: 'fileLeafRef',
|
||||
minWidth: 200,
|
||||
isRowHeader: true,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
sortAscendingAriaLabel: strings.SortedAscending,
|
||||
sortDescendingAriaLabel: strings.SortedDescending,
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'string',
|
||||
isPadded: true,
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
// If this is a folder, browse to that folder
|
||||
if (item.isFolder) {
|
||||
return <span className={styles.folderItem} onClick={(_event) => this._handleOpenLibrary(item)}>{item.name}</span>;
|
||||
} else {
|
||||
// Just show the file name
|
||||
return <span className={styles.fileItem}>{item.name}</span>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'colModified',
|
||||
name: strings.ODModifiedField,
|
||||
fieldName: 'modified',
|
||||
minWidth: 120,
|
||||
isResizable: true,
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'number',
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
//aria-label="Modified column, March 9, 2017"
|
||||
const ariaLabel: string = strings.AriaCellValue.replace('{0}', strings.ODModifiedField).replace('{1}', item.modified);
|
||||
|
||||
// Modified date already returns localized and formatted
|
||||
return <span aria-label={ariaLabel} >{item.modified}</span>;
|
||||
},
|
||||
isPadded: true
|
||||
},
|
||||
{
|
||||
key: 'colEditor',
|
||||
name: strings.ModifiedByField,
|
||||
fieldName: 'modifiedBy',
|
||||
minWidth: 100,
|
||||
isResizable: true,
|
||||
data: 'string',
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
return <span>{item.modifiedBy}</span>;
|
||||
},
|
||||
isPadded: true
|
||||
},
|
||||
{
|
||||
key: 'colSize',
|
||||
name: strings.FileSizeField,
|
||||
fieldName: 'fileSizeDisplay',
|
||||
minWidth: 100,
|
||||
isResizable: true,
|
||||
data: 'number',
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
// Format the size into a nice human-friendly format.
|
||||
return <span>{item.fileSizeDisplay ? FormatBytes(item.fileSizeDisplay, 1) : undefined}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'colShared',
|
||||
name: strings.SharingField,
|
||||
fieldName: 'principalCount',
|
||||
minWidth: 100,
|
||||
isResizable: true,
|
||||
data: 'string',
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IOneDriveFile) => {
|
||||
// Can't find any references here, but I'm pretty sure that
|
||||
// if there are no other principals, nobody else is allowed to see it
|
||||
// but anything above zero means we shared with at least one person/group.
|
||||
|
||||
const cellValue: string = item.isShared ? strings.SharingShared : strings.SharingPrivate;
|
||||
|
||||
const ariaLabel: string = strings.AriaCellValue.replace('{0}', strings.SharingField).replace('{1}', cellValue);
|
||||
|
||||
|
||||
return <span aria-label={ariaLabel}>
|
||||
{item.isShared && <span>
|
||||
<Icon iconName="People" />
|
||||
<span> </span>
|
||||
</span>}
|
||||
{cellValue}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
this._selection = new Selection(
|
||||
{
|
||||
selectionMode: SelectionMode.single,
|
||||
onSelectionChanged: () => {
|
||||
// Get the selected item
|
||||
const selectedItems = this._selection.getSelection();
|
||||
|
||||
if (selectedItems && selectedItems.length > 0) {
|
||||
const selectedKey: IOneDriveFile = selectedItems[0] as IOneDriveFile;
|
||||
if (!selectedKey.isFolder) {
|
||||
// Save the selected file
|
||||
this.setState({
|
||||
fileUrl: selectedKey.absoluteUrl
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Remove any selected file
|
||||
this.setState({
|
||||
fileUrl: undefined
|
||||
});
|
||||
}
|
||||
if (this._listElem) {
|
||||
// Force the list to update to show the selection check
|
||||
this._listElem.forceUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
files: [],
|
||||
hideDialog: true,
|
||||
parentFolderInfo: [],
|
||||
selectedView: lastLayout,
|
||||
columns: columns
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OneDrive files
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
// Initialize the OneDrive services
|
||||
this._oneDriveService = new OneDriveServices(this.props.context, this.props.accepts);
|
||||
|
||||
// Get the items at the root of the OneDrive folder
|
||||
this._getListItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle if we change the relative folder we're browsing
|
||||
*/
|
||||
public componentDidUpdate(_prevProps: IOneDriveTabProps, prevState: IOneDriveTabState): void {
|
||||
// Update the list of items if the folder changes
|
||||
if (prevState.serverRelativeFolderUrl !== this.state.serverRelativeFolderUrl) {
|
||||
this._getListItems();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of items to display
|
||||
*/
|
||||
private _getListItems = (): Promise<void> => {
|
||||
// We're loading!
|
||||
this.setState({
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
return this._oneDriveService.GetListDataAsStream(this.state.serverRelativeFolderUrl).then((listDataStream: IGetListDataAsStreamResult) => {
|
||||
|
||||
// Get the thumbnail URL template -- stored in the list schema
|
||||
const thumbnailUrlTemplate: string = listDataStream.ListSchema[".thumbnailUrl"]
|
||||
.replace("{.mediaBaseUrl}", listDataStream.ListSchema[".mediaBaseUrl"])
|
||||
.replace("{.callerStack}", listDataStream.ListSchema[".callerStack"])
|
||||
.replace("{.driveAccessToken}", "encodeFailures=1&ctag={.ctag}");
|
||||
|
||||
// Map every item to a OneDrive file
|
||||
const files: IOneDriveFile[] = listDataStream.ListData.Row.map((item: IRow) => {
|
||||
// Build the thumbnail URL from the template
|
||||
// The template is stored in the schema (see above) and contains list-specific
|
||||
// replacement tokens (which we already replaced above) and item-specific
|
||||
// tokens, which we're replacing right. now.
|
||||
const thumbnail: string = thumbnailUrlTemplate
|
||||
.replace('{.spItemUrl}', item[".spItemUrl"])
|
||||
.replace('{.ctag}', encodeURIComponent(item[".ctag"]))
|
||||
.replace('{.fileType}', item[".fileType"]);
|
||||
|
||||
// Get the modified date
|
||||
const modifiedParts: string[] = item["Modified.FriendlyDisplay"]!.split('|');
|
||||
let modified: string = item.Modified;
|
||||
|
||||
// If there is a friendly modified date, use that
|
||||
// The friendly dates seem to be a lot smarter than what I have here.
|
||||
// For example, it seems to use a different structure for dates that are on the same
|
||||
// day, within a few days, etc.
|
||||
// For this example, we just handle the regular friendly-dates, but if we
|
||||
// turn this into a PnP control, we'll want to handle all sorts of friendly dates
|
||||
if (modifiedParts.length === 2) {
|
||||
modified = modifiedParts[1];
|
||||
}
|
||||
|
||||
// Parse media metadata to see if we can get known dimensions
|
||||
// Dimensions are stored as HTML-encoded JSON from media services.
|
||||
// If it is available, get the JSON structure and parse it.
|
||||
const media: any = item.MediaServiceFastMetadata && JSON.parse(unescape(item.MediaServiceFastMetadata));
|
||||
const dimensions: IDimensions = media && media.photo && {
|
||||
width: media.photo.width,
|
||||
height: media.photo.height
|
||||
};
|
||||
|
||||
// Create a nice OneDriveFile interface so we're not saving all that extra metadata that
|
||||
// gets returned from SharePoint in our state.
|
||||
const file: IOneDriveFile = {
|
||||
key: item.UniqueId,
|
||||
name: item.FileLeafRef,
|
||||
absoluteUrl: this._buildOneDriveAbsoluteUrl(listDataStream.HttpRoot, item.FileRef),
|
||||
serverRelativeUrl: item.FileRef,
|
||||
isFolder: item.FSObjType === "1",
|
||||
modified: modified,
|
||||
modifiedBy: item.Editor[0].title,
|
||||
fileType: item.File_x0020_Type,
|
||||
fileIcon: item["HTML_x0020_File_x0020_Type.File_x0020_Type.mapico"],
|
||||
fileSizeDisplay: item.FileSizeDisplay,
|
||||
totalFileCount: +item.SMTotalFileCount, // quickly converts string to number
|
||||
thumbnail: thumbnail,
|
||||
dimensions: dimensions,
|
||||
isShared: parseInt(item.PrincipalCount) > 0
|
||||
};
|
||||
return file;
|
||||
});
|
||||
|
||||
// Set the selection items so that we know what item we're selecting
|
||||
this._selection.setItems(files);
|
||||
|
||||
// Store the files and stop the loading icon
|
||||
this.setState({
|
||||
files: files,
|
||||
isLoading: false, // we're done loading
|
||||
parentFolderInfo: listDataStream.ParentInfo.ParentFolderInfo // remember where we are
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the tab
|
||||
*/
|
||||
public render(): React.ReactElement<IOneDriveTabProps> {
|
||||
const {
|
||||
isLoading,
|
||||
files,
|
||||
selectedView,
|
||||
fileUrl } = this.state;
|
||||
|
||||
return (
|
||||
<div className={css(styles.tabContainer)}>
|
||||
<div className={styles.tabHeaderContainer}>
|
||||
{this._onRenderHeader()}
|
||||
{this._onRenderCommandBar()}
|
||||
</div>
|
||||
<div className={styles.tab}>
|
||||
{isLoading && <Spinner label={strings.Loading} />}
|
||||
{!isLoading && files!.length > 0 &&
|
||||
selectedView !== 'tiles' && this._renderListLayout()}
|
||||
{!isLoading && files!.length > 0 &&
|
||||
selectedView === 'tiles' && this._renderTileLayout()}
|
||||
|
||||
{!isLoading && files!.length < 1 &&
|
||||
this._renderEmptyFolder()
|
||||
}
|
||||
</div>
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
<div className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={!fileUrl}
|
||||
onClick={() => this._handleSaveConfirm()} className={styles.actionButton}>{strings.OpenButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
{this._onRenderConfirmDialog()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gratuitous sorting
|
||||
*/
|
||||
private _onColumnClick = (event: React.MouseEvent<HTMLElement>, column: IColumn): void => {
|
||||
const { columns } = this.state;
|
||||
let { files } = this.state;
|
||||
let isSortedDescending = column.isSortedDescending;
|
||||
|
||||
// If we've sorted this column, flip it.
|
||||
if (column.isSorted) {
|
||||
isSortedDescending = !isSortedDescending;
|
||||
}
|
||||
|
||||
// Sort the items.
|
||||
files = files!.concat([]).sort((a, b) => {
|
||||
const firstValue = a[column.fieldName || ''];
|
||||
const secondValue = b[column.fieldName || ''];
|
||||
|
||||
if (isSortedDescending) {
|
||||
return firstValue > secondValue ? -1 : 1;
|
||||
} else {
|
||||
return firstValue > secondValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset the items and columns to match the state.
|
||||
this._selection.setItems(files);
|
||||
this.setState({
|
||||
files: files,
|
||||
columns: columns!.map(col => {
|
||||
col.isSorted = col.key === column.key;
|
||||
|
||||
if (col.isSorted) {
|
||||
col.isSortedDescending = isSortedDescending;
|
||||
}
|
||||
|
||||
return col;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a tile
|
||||
*/
|
||||
private _renderTileLayout = (): JSX.Element => {
|
||||
const { files } = this.state;
|
||||
|
||||
return (<SelectionZone selection={this._selection}
|
||||
onItemInvoked={(item: IOneDriveFile) => this._handleItemInvoked(item)}
|
||||
>
|
||||
<FocusZone>
|
||||
<List
|
||||
ref={this._linkList}
|
||||
className={styles.folderList}
|
||||
items={files}
|
||||
getItemCountForPage={this._getItemCountForPage}
|
||||
getPageHeight={this._getPageHeight}
|
||||
renderedWindowsAhead={4}
|
||||
onRenderPage={(pageProps: IPageProps, defaultRender?: IRenderFunction<IPageProps>) => this._onRenderPage(pageProps, defaultRender)}
|
||||
/>
|
||||
</FocusZone>
|
||||
</SelectionZone>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a list or a compact list
|
||||
*/
|
||||
private _renderListLayout = (): JSX.Element => {
|
||||
const { files, selectedView, columns } = this.state;
|
||||
return (
|
||||
<DetailsList
|
||||
items={files}
|
||||
compact={selectedView === 'compact'}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.single}
|
||||
setKey="set"
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
isHeaderVisible={true}
|
||||
selection={this._selection}
|
||||
selectionPreservedOnEmptyClick={true}
|
||||
onActiveItemChanged={(item: IOneDriveFile, index: number, ev: React.FormEvent<Element>) => this._itemChangedHandler(item, index, ev)}
|
||||
enterModalSelectionOnTouch={true}
|
||||
onRenderRow={this._onRenderRow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a row in a detailed list
|
||||
*/
|
||||
private _onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
const fileItem: IOneDriveFile = props.item;
|
||||
|
||||
return <DetailsRow
|
||||
getRowAriaLabel={(item: IOneDriveFile) => this._getRowAriaLabel(item)}
|
||||
{...props}
|
||||
className={fileItem.isFolder ?
|
||||
styles.folderRow
|
||||
: styles.fileRow} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called on every row to provide an ARIA label describing the data in the row.
|
||||
*/
|
||||
private _getRowAriaLabel = (item: IOneDriveFile): string => {
|
||||
//Attachments, Folder, Modified March 9, 2017, edited by Hugo Bernier, 0 items, Private
|
||||
//me.png, .png Image, Modified December 12, 2017, edited by Hugo Bernier, 86.5 KB, Shared
|
||||
//"{0}, {1}, Modified {2}, edited by {3}, {4}, {5}"
|
||||
|
||||
const fileType: string = item.isFolder ? strings.FolderAltText : strings.ImageAltText.replace('{0}', item.fileType);
|
||||
const sharingValue: string = item.isShared ? strings.SharingShared : strings.SharingPrivate;
|
||||
|
||||
const ariaLabel: string = strings.ODRowArialLabelTemplate.replace('{0}', item.name)
|
||||
.replace('{1}', fileType)
|
||||
.replace('{2}', item.modified)
|
||||
.replace('{3}', item.modifiedBy)
|
||||
.replace('{4}', item.fileSizeDisplay)
|
||||
.replace('{5}', sharingValue);
|
||||
return ariaLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the command bar above the list/grid
|
||||
*/
|
||||
private _onRenderCommandBar = (): JSX.Element => {
|
||||
return (<div className={styles.itemPickerTopBar}>
|
||||
<CommandBar
|
||||
items={this._getToolbarItems()}
|
||||
farItems={this.getFarItems()}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an annoying pop-up saying you should make sure
|
||||
* that you shared this file otherwise people won't see it
|
||||
*/
|
||||
private _onRenderConfirmDialog = (): JSX.Element => {
|
||||
return (<Dialog
|
||||
hidden={this.state.hideDialog}
|
||||
dialogContentProps={{
|
||||
type: DialogType.normal,
|
||||
title: strings.OneDriveConfirmDialogTitle,
|
||||
subText: strings.OneDriveConfirmDialogBody
|
||||
}}
|
||||
modalProps={{
|
||||
isBlocking: true,
|
||||
containerClassName: styles.dialogMainOverride
|
||||
}}
|
||||
>
|
||||
<DialogFooter>
|
||||
<PrimaryButton onClick={(_ev) => this._handleSave()} text={strings.Yes} />
|
||||
<DefaultButton onClick={(_ev) => this._handleClose()} text={strings.No} />
|
||||
</DialogFooter>
|
||||
</Dialog>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a heading or a breadcrumb (depending on how many levels deep we're in)
|
||||
*/
|
||||
private _onRenderHeader = (): JSX.Element => {
|
||||
const { parentFolderInfo } = this.state;
|
||||
|
||||
// If we don't have parent info, or we're only 2 levels deep, don't render breadcrumbs.
|
||||
// just render a heading.
|
||||
// also, render a header until we're fully loaded, otherwise the breadcrumb starts
|
||||
// flickering.
|
||||
if (parentFolderInfo === undefined || parentFolderInfo.length < 2 || this.state.isLoading) {
|
||||
return (<h2 className={styles.tabHeader}>{this.state.folderName ? this.state.folderName : strings.OneDriveRootFolderName}</h2>);
|
||||
}
|
||||
|
||||
// Get the breadcrumb path
|
||||
const breadCrumbItems: IBreadcrumbItem[] = [];
|
||||
|
||||
// Parent info comes in reversed, so reverse it to render the breadcrumbs in the right order
|
||||
parentFolderInfo.reverse().forEach((parentFolder: IParentFolderInfo, index: number) => {
|
||||
// Anything after the first level is a link
|
||||
if (index > 1) {
|
||||
const folderName: string = this._getFolderName(parentFolder.ServerRelativeUrl);
|
||||
|
||||
breadCrumbItems.push({
|
||||
text: folderName,
|
||||
key: folderName,
|
||||
onClick: () => this._handleOpenLibraryByPath(parentFolder.ServerRelativeUrl, folderName)
|
||||
});
|
||||
} else if (index === 1) {
|
||||
// First level is always a link to the OneDrive root
|
||||
breadCrumbItems.push({
|
||||
text: strings.OneDriveRootFolderName,
|
||||
key: strings.OneDriveRootFolderName,
|
||||
onClick: () => this._handleOpenLibraryByPath(parentFolder.ServerRelativeUrl, strings.OneDriveRootFolderName)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// List breadcrumb node is the current folder
|
||||
breadCrumbItems.push({ text: this.state.folderName, key: this.state.folderName, isCurrentItem: true });
|
||||
|
||||
return <Breadcrumb
|
||||
className={styles.standaloneListBreadcrumb}
|
||||
items={breadCrumbItems}
|
||||
/>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a custom list page
|
||||
*/
|
||||
private _onRenderPage = (pageProps: IPageProps, _defaultRender?: IRenderFunction<IPageProps>): JSX.Element => {
|
||||
const {
|
||||
page,
|
||||
className: pageClassName,
|
||||
...divProps
|
||||
} = pageProps;
|
||||
|
||||
const { items } = page;
|
||||
|
||||
return <div {...divProps} className={css(pageClassName, styles.listPage)}>
|
||||
<div className={styles.grid}
|
||||
style={{
|
||||
width: this._pageWidth,
|
||||
marginTop: -STANDARD_TILE_MARGIN,
|
||||
marginBottom: BOTTOM_MARGIN,
|
||||
marginLeft: -STANDARD_TILE_MARGIN,
|
||||
marginRight: -STANDARD_TILE_MARGIN
|
||||
}}
|
||||
>
|
||||
{items.map((item: IOneDriveFile, index: number) => {
|
||||
return this._onRenderCell(item, index);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a placeholder to indicate that the folder is empty
|
||||
*/
|
||||
private _renderEmptyFolder = (): JSX.Element => {
|
||||
return (<div className={styles.emptyFolder}>
|
||||
<div className={styles.emptyFolderImage}>
|
||||
<img
|
||||
className={styles.emptyFolderImageTag}
|
||||
src={strings.OneDriveEmptyFolderIconUrl}
|
||||
alt={strings.OneDriveEmptyFolderAlt} />
|
||||
</div>
|
||||
<div role="alert">
|
||||
<div className={styles.emptyFolderTitle}>
|
||||
{strings.OneDriveEmptyFolderTitle}
|
||||
</div>
|
||||
<div className={styles.emptyFolderSubText}>
|
||||
<span className={styles.emptyFolderPc}>
|
||||
{strings.OneDriveEmptyFolderDescription}
|
||||
</span>
|
||||
{/* Removed until we add support to upload */}
|
||||
{/* <span className={styles.emptyFolderMobile}>
|
||||
Tap <Icon iconName="Add" className={styles.emptyFolderIcon} /> to add files here.
|
||||
</span> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the list of toolbar items on the left side of the toolbar.
|
||||
* We leave it empty for now, but we may add the ability to upload later.
|
||||
*/
|
||||
private _getToolbarItems = (): ICommandBarItemProps[] => {
|
||||
return [
|
||||
// This space for rent
|
||||
];
|
||||
}
|
||||
|
||||
private getFarItems = (): ICommandBarItemProps[] => {
|
||||
const { selectedView } = this.state;
|
||||
|
||||
let viewIconName: string = undefined;
|
||||
let viewName: string = undefined;
|
||||
switch (this.state.selectedView) {
|
||||
case 'list':
|
||||
viewIconName = 'List';
|
||||
viewName = strings.ListLayoutList;
|
||||
break;
|
||||
case 'compact':
|
||||
viewIconName = 'AlignLeft';
|
||||
viewName = strings.ListLayoutCompact;
|
||||
break;
|
||||
default:
|
||||
viewIconName = 'GridViewMedium';
|
||||
viewName = strings.ListLayoutTile;
|
||||
}
|
||||
|
||||
const farItems: ICommandBarItemProps[] = [
|
||||
{
|
||||
key: 'listOptions',
|
||||
className: styles.commandBarNoChevron,
|
||||
title: strings.ListOptionsTitle,
|
||||
ariaLabel: strings.ListOptionsAlt.replace('{0}', viewName),
|
||||
iconProps: {
|
||||
iconName: viewIconName
|
||||
},
|
||||
iconOnly: true,
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: 'list',
|
||||
name: strings.ListLayoutList,
|
||||
iconProps: {
|
||||
iconName: 'List'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'list',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutList).replace('{1}', selectedView === 'list' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutListDescrition,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
},
|
||||
{
|
||||
key: 'compact',
|
||||
name: strings.ListLayoutCompact,
|
||||
iconProps: {
|
||||
iconName: 'AlignLeft'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'compact',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutCompact).replace('{1}', selectedView === 'compact' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutCompactDescription,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
},
|
||||
{
|
||||
key: 'tiles',
|
||||
name: 'Tiles',
|
||||
iconProps: {
|
||||
iconName: 'GridViewMedium'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'tiles',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutTile).replace('{1}', selectedView === 'tiles' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutTileDescription,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
return farItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when users switch the view
|
||||
*/
|
||||
private _handleSwitchLayout = (item?: IContextualMenuItem) => {
|
||||
if (item) {
|
||||
// Store the user's favourite layout
|
||||
if (localStorage) {
|
||||
localStorage.setItem(LAYOUT_STORAGE_KEY, item.key);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedView: item.key as ViewType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called what a file is selected.
|
||||
*/
|
||||
private _handleItemInvoked = (item: IOneDriveFile) => {
|
||||
// If a file is selected, open the library
|
||||
if (item.isFolder) {
|
||||
this._handleOpenLibrary(item);
|
||||
} else {
|
||||
// Otherwise, remember it was selected
|
||||
this._selection.setKeySelected(item.key, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When user selects an item, save selection
|
||||
*/
|
||||
private _itemChangedHandler = (item: IOneDriveFile, _index: number, _ev): void => {
|
||||
// When we highlight a folder, do nothing (except set the selection to blank)
|
||||
if (item.isFolder) {
|
||||
this.setState({
|
||||
fileUrl: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// set the item selected
|
||||
this._selection.setKeySelected(item.key, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many items there should be in the page
|
||||
*/
|
||||
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
|
||||
if (itemIndex === 0) {
|
||||
this._columnCount = Math.ceil(surfaceRect.width / MAX_ROW_HEIGHT);
|
||||
this._columnWidth = Math.floor(surfaceRect.width / this._columnCount);
|
||||
this._rowHeight = this._columnWidth;
|
||||
this._pageWidth = surfaceRect.width;
|
||||
}
|
||||
|
||||
// Get the list of items
|
||||
const { files } = this.state;
|
||||
const isFolder: boolean = files[itemIndex].isFolder;
|
||||
|
||||
// Group items by folders and files
|
||||
let pageLength: number = 0;
|
||||
for (let index = itemIndex; index < files.length; index++) {
|
||||
const element = files[index];
|
||||
if (element.isFolder === isFolder) {
|
||||
pageLength++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the page lenght, up to the maximum number of cells per page
|
||||
return Math.min(pageLength, CELLS_PER_PAGE);
|
||||
}
|
||||
|
||||
/** Calculates the list "page" height (a.k.a. row) */
|
||||
private _getPageHeight = (): number => {
|
||||
return this._rowHeight * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a file folder cover
|
||||
*/
|
||||
private _onRenderCell = (item: IOneDriveFile, index: number | undefined): JSX.Element => {
|
||||
let isSelected: boolean = false;
|
||||
|
||||
if (this._selection && index !== undefined) {
|
||||
isSelected = this._selection.isIndexSelected(index);
|
||||
}
|
||||
|
||||
// I know this is a lot of divs and spans inside of each other, but my
|
||||
// goal was to mimic the HTML and style of the out-of-the-box file picker
|
||||
// to the best of my ability.
|
||||
return (
|
||||
<div
|
||||
className={styles.listCell}
|
||||
data-item-index={index}
|
||||
style={{
|
||||
flexBasis: this._columnWidth,
|
||||
maxWidth: this._columnWidth,
|
||||
margin: STANDARD_TILE_MARGIN,
|
||||
borderStyle: "none",
|
||||
borderWidth: 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="presentation"
|
||||
className={styles.cell}
|
||||
|
||||
// I don't agree with this magic number. Where does this come from?
|
||||
style={{ paddingTop: "97.16%" }}
|
||||
>
|
||||
<div role="presentation" className={styles.cellContent}>
|
||||
{item.isFolder ? <FolderTile
|
||||
item={item}
|
||||
index={index}
|
||||
isSelected={isSelected}
|
||||
pageWidth={this._pageWidth}
|
||||
tileDimensions={{
|
||||
width: this._columnWidth - TILE_HORZ_PADDING,
|
||||
height: this._rowHeight - TILE_HORZ_PADDING
|
||||
}}
|
||||
onItemInvoked={(itemInvoked: IOneDriveFile) => this._handleItemInvoked(itemInvoked)}
|
||||
/>
|
||||
: <DocumentTile
|
||||
item={item}
|
||||
index={index}
|
||||
isSelected={isSelected}
|
||||
pageWidth={this._pageWidth}
|
||||
tileDimensions={{
|
||||
width: this._columnWidth - TILE_HORZ_PADDING,
|
||||
height: this._rowHeight - TILE_HORZ_PADDING
|
||||
}}
|
||||
onItemInvoked={(itemInvoked: IOneDriveFile) => this._handleItemInvoked(itemInvoked)}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an absolute URL
|
||||
*/
|
||||
private _buildOneDriveAbsoluteUrl = (root: string, relativeUrl: string) => {
|
||||
const siteUrl: string = GetAbsoluteDomainUrl(root);
|
||||
return siteUrl + relativeUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls parent when library is opened
|
||||
*/
|
||||
private _handleOpenLibrary = (library: IOneDriveFile) => {
|
||||
this.setState({
|
||||
serverRelativeFolderUrl: library.serverRelativeUrl,
|
||||
folderName: library.name
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when someone clicks on a breadcrumb
|
||||
* In this case, we only have the path to work with, not the full
|
||||
* reference to the OneDrive file
|
||||
*/
|
||||
private _handleOpenLibraryByPath = (serverRelativeUrl: string, libraryName: string) => {
|
||||
this.setState({
|
||||
serverRelativeFolderUrl: serverRelativeUrl,
|
||||
folderName: libraryName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user with dialog before they can save
|
||||
*/
|
||||
private _handleSaveConfirm = () => {
|
||||
this.setState({
|
||||
hideDialog: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user saves
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user closes tab
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ref to the list
|
||||
*/
|
||||
private _linkList = (e: any) => {
|
||||
this._listElem = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the last part of the path to get the folder name
|
||||
*/
|
||||
private _getFolderName = (folderPath: string): string => {
|
||||
// break the folder path into its sub-folders
|
||||
const pathSegments: string[] = folderPath.split('/');
|
||||
return pathSegments.pop();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
import { IParentFolderInfo, IDimensions } from '../../../services/OneDriveServices';
|
||||
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
|
||||
export interface IOneDriveTabProps extends IFilePickerTab {
|
||||
//inherited
|
||||
}
|
||||
|
||||
export interface IOneDriveTabState {
|
||||
isLoading: boolean;
|
||||
files: IOneDriveFile[];
|
||||
fileUrl?: string;
|
||||
serverRelativeFolderUrl?: string;
|
||||
folderName?: string;
|
||||
hideDialog: boolean;
|
||||
parentFolderInfo: IParentFolderInfo[];
|
||||
selectedView: ViewType;
|
||||
columns: IColumn[];
|
||||
}
|
||||
|
||||
export interface IOneDriveFile {
|
||||
name: string;
|
||||
absoluteUrl: string;
|
||||
serverRelativeUrl: string;
|
||||
isFolder: boolean;
|
||||
modified: string;
|
||||
modifiedBy: string;
|
||||
fileType: string;
|
||||
fileIcon: string;
|
||||
fileSizeDisplay: string;
|
||||
totalFileCount: number;
|
||||
key: string;
|
||||
thumbnail: string;
|
||||
dimensions?: IDimensions;
|
||||
isShared: boolean;
|
||||
}
|
||||
|
||||
export type ViewType = 'list' | 'compact' | 'tiles';
|
|
@ -0,0 +1,2 @@
|
|||
export * from './OneDriveTab';
|
||||
export * from './OneDriveTab.types';
|
|
@ -0,0 +1,376 @@
|
|||
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
|
||||
|
||||
.filePicker {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Panel-commands) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Panel-main) {
|
||||
top: 50px;
|
||||
border: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
[dir="ltr"] .filePicker :global(.ms-Panel-content) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Panel-contentInner) {
|
||||
padding: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Panel-header) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filePicker
|
||||
:global(.ms-Panel-main)
|
||||
:global(.ms-Panel-contentInner)
|
||||
:global(.ms-Panel-content) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Nav-link) {
|
||||
position: relative;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-top: 0px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 15px;
|
||||
height: 36px;
|
||||
color: $ms-color-neutralPrimary;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
line-height: 36px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
outline: transparent;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
text-decoration: none;
|
||||
border-radius: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filePicker :global(.is-selected) :global(.ms-Nav-link) {
|
||||
position: relative;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
padding-top: 0px;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 15px;
|
||||
height: 36px;
|
||||
color: $ms-color-themePrimary;
|
||||
background-color: $ms-color-neutralLighter;
|
||||
width: 100%;
|
||||
line-height: 36px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
outline: transparent;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
text-decoration: none;
|
||||
border-radius: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tabsContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
height: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[dir="ltr"] .tabsContainer {
|
||||
left: 212px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .tabsContainer {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 100%;
|
||||
color: $ms-color-neutralLighter;
|
||||
}
|
||||
|
||||
[dir="ltr"] .nav {
|
||||
border-right: 2px solid;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.nav :global(.ms-Nav) {
|
||||
height: 100%;
|
||||
width: 210px;
|
||||
padding-top: 60px;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
border-top-width: 40px;
|
||||
border-top-style: solid;
|
||||
color: $ms-color-neutralLighter;
|
||||
}
|
||||
|
||||
.tabHeaderContainer {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
color: $ms-color-neutralLighter;
|
||||
border-top: 40px solid;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0 32px;
|
||||
overflow-y: auto;
|
||||
-webkit-box-flex: 2;
|
||||
-ms-flex-positive: 2;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.actionButtonsContainer {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: $ms-color-white;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
height: 30px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .actionButton {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .actionButton {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
[dir="ltr"] .actionButton {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .actionButtons {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .actionButtons {
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
.tabHeader {
|
||||
color: $ms-color-neutralPrimary;
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 100;
|
||||
padding: 22px 0 20px 32px;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
height: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filePicker :global(.ms-Panel-main) {
|
||||
top: 50px;
|
||||
border: none;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.tabsContainer .selectedTab {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.xlg, .xxlg, .xxxlg, .xxxxlg {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.itemPickerTopBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.commandBarNoChevron :global(.ms-CommandBarItem-chevronDown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.fileItem {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.folderItem {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folderItem:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.fileTypeIcon {
|
||||
margin: 0 -2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.fileTypeIconIcon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.detailsListHeaderFileTypeIcon {
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.iconColumnHeader {
|
||||
width: 40px!important;
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.iconColumnHeader i {
|
||||
font-size:16px;
|
||||
}
|
||||
|
||||
.fileRow {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.folderRow {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.folderRow :global(.ms-DetailsRow-check) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.emptyFolder {
|
||||
text-align: center;
|
||||
|
||||
.emptyFolderImage {
|
||||
padding-top: 52px;
|
||||
height: 208px;
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
|
||||
.emptyFolderImageTag {
|
||||
height: 208px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyFolderTitle {
|
||||
padding: 16px 16px 0 16px;
|
||||
font-size: 21px;
|
||||
font-weight: 100;
|
||||
max-width: 400px;
|
||||
color: #858585;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.emptyFolderSubText {
|
||||
padding: 6px 16px 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
max-width: 400px;
|
||||
color: #858585;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.emptyFolderMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emptyFolderPc {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.emptyFolderIcon {
|
||||
display: inline;
|
||||
font-size: 13px;
|
||||
padding-left: 2px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import {
|
||||
IPropertyPaneField,
|
||||
PropertyPaneFieldType
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import { PropertyPaneFilePickerHost, IPropertyPaneFilePickerPropsInternal, IPropertyPaneFilePickerProps } from '.';
|
||||
|
||||
class PropertyPaneFilePickerBuilder implements IPropertyPaneField<IPropertyPaneFilePickerPropsInternal> {
|
||||
public targetProperty: string;
|
||||
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
|
||||
public properties: IPropertyPaneFilePickerPropsInternal;
|
||||
|
||||
private _onChangeCallback: (targetProperty?: string, newValue?: any) => void;
|
||||
|
||||
public constructor(_targetProperty: string, _properties: IPropertyPaneFilePickerPropsInternal) {
|
||||
this.targetProperty = _targetProperty;
|
||||
this.properties = _properties;
|
||||
|
||||
this.properties.onRender = this.render.bind(this);
|
||||
this.properties.onDispose = this.dispose.bind(this);
|
||||
}
|
||||
|
||||
private render(elem: HTMLElement, context?: any, changeCallback?: (targetProperty?: string, newValue?: any[]) => void): void {
|
||||
const props: IPropertyPaneFilePickerProps = <IPropertyPaneFilePickerProps>this.properties;
|
||||
|
||||
const element = React.createElement(PropertyPaneFilePickerHost, {
|
||||
...props,
|
||||
onChanged: this.onChanged.bind(this)
|
||||
});
|
||||
|
||||
ReactDOM.render(element, elem);
|
||||
|
||||
if (changeCallback) {
|
||||
this._onChangeCallback = changeCallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the property field
|
||||
*/
|
||||
private dispose(elem: HTMLElement) {
|
||||
ReactDOM.unmountComponentAtNode(elem);
|
||||
}
|
||||
|
||||
/**
|
||||
* On field change event handler
|
||||
* @param value
|
||||
*/
|
||||
private onChanged(value: any[]): void {
|
||||
if (this._onChangeCallback) {
|
||||
this._onChangeCallback(this.targetProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Property field
|
||||
* @param targetProperty
|
||||
* @param properties
|
||||
*/
|
||||
export function PropertyPaneFilePicker(targetProperty: string, properties: IPropertyPaneFilePickerProps): IPropertyPaneField<IPropertyPaneFilePickerPropsInternal> {
|
||||
return new PropertyPaneFilePickerBuilder(targetProperty, {
|
||||
...properties,
|
||||
onRender: null,
|
||||
onDispose: null
|
||||
});
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Our custom styles
|
||||
import styles from './PropertyPaneFilePicker.module.scss';
|
||||
|
||||
// Our props
|
||||
import { IPropertyPaneFilePickerHostProps, IPropertyPaneFilePickerHostState } from '.';
|
||||
|
||||
// Office Fabric controls
|
||||
import { PrimaryButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/components/Panel';
|
||||
import { Label } from 'office-ui-fabric-react/lib/components/Label';
|
||||
import { Nav, INavLink, INavLinkGroup } from 'office-ui-fabric-react/lib/Nav';
|
||||
import { css } from "@uifabric/utilities/lib/css";
|
||||
|
||||
// Localization
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
// Custom property pane file picker tabs
|
||||
import LinkFilePickerTab from './LinkFilePickerTab/LinkFilePickerTab';
|
||||
import UploadFilePickerTab from './UploadFilePickerTab/UploadFilePickerTab';
|
||||
import SiteFilePickerTab from './SiteFilePickerTab/SiteFilePickerTab';
|
||||
import WebSearchTab from './WebSearchTab/WebSearchTab';
|
||||
import RecentFilesTab from './RecentFilesTab/RecentFilesTab';
|
||||
import { ItemType } from './IPropertyPaneFilePicker';
|
||||
import OneDriveTab from './OneDriveTab/OneDriveTab';
|
||||
|
||||
// The list of acceptable image file types
|
||||
const ACCEPTABLE_IMAGEFILE_EXTENSIONS: string = ".gif,.jpg,.jpeg,.bmp,.dib,.tif,.tiff,.ico,.png,.jxr,.svg";
|
||||
|
||||
export class PropertyPaneFilePickerHost extends React.Component<IPropertyPaneFilePickerHostProps, IPropertyPaneFilePickerHostState> {
|
||||
constructor(props: IPropertyPaneFilePickerHostProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
panelOpen: false,
|
||||
selectedTab: 'keyRecent',
|
||||
showFullNav: true
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
// If no acceptable file type was passed, and we're expecting images, set the default image filter
|
||||
const accepts: string = this.props.accepts !== undefined ? this.props.accepts
|
||||
: this.props.itemType === ItemType.Images && this.props.accepts === undefined ? ACCEPTABLE_IMAGEFILE_EXTENSIONS
|
||||
: undefined;
|
||||
|
||||
// Get a list of groups to display
|
||||
let groups: INavLinkGroup[] = [
|
||||
{
|
||||
links: [
|
||||
{
|
||||
name: strings.RecentLinkLabel,
|
||||
url: '#recent',
|
||||
icon: 'Recent',
|
||||
key: 'keyRecent',
|
||||
},
|
||||
{
|
||||
name: strings.WebSearchLinkLabel,
|
||||
url: '#search',
|
||||
key: 'keyWeb',
|
||||
icon: 'Search',
|
||||
},
|
||||
{
|
||||
name: "OneDrive",
|
||||
url: '#onedrive',
|
||||
key: 'keyOneDrive',
|
||||
icon: 'OneDrive',
|
||||
},
|
||||
{
|
||||
name: strings.SiteLinkLabel,
|
||||
url: '#globe',
|
||||
key: 'keySite',
|
||||
icon: 'Globe',
|
||||
},
|
||||
{
|
||||
name: strings.UploadLinkLabel,
|
||||
url: '#upload',
|
||||
key: 'keyUpload',
|
||||
icon: 'System'
|
||||
},
|
||||
{
|
||||
name: strings.FromLinkLinkLabel,
|
||||
url: '#link',
|
||||
key: 'keyLink',
|
||||
icon: 'Link'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Hide tabs we don't want. Start from bottom of the list
|
||||
// so we're not changing the relative position of items
|
||||
// as we remove them.
|
||||
|
||||
// I'm sure there is a better way to do this...
|
||||
|
||||
// If we don't want local uploads, remove it from the list
|
||||
if (this.props.disableLocalUpload) {
|
||||
groups[0].links.splice(4, 1);
|
||||
}
|
||||
|
||||
// If we don't want OneDrive, remove it from the list
|
||||
if (this.props.hasMySiteTab === false) {
|
||||
groups[0].links.splice(2, 1);
|
||||
}
|
||||
|
||||
// If we don't want web search, remove it from the list
|
||||
if (this.props.disableWebSearchTab) {
|
||||
groups[0].links.splice(1, 1);
|
||||
}
|
||||
|
||||
return (
|
||||
<div >
|
||||
<Label required={this.props.required}>{this.props.label}</Label>
|
||||
<PrimaryButton text={this.props.buttonLabel}
|
||||
onClick={this._handleOpenPanel}
|
||||
disabled={this.props.disabled} />
|
||||
|
||||
<Panel isOpen={this.state.panelOpen}
|
||||
isBlocking={true}
|
||||
hasCloseButton={true}
|
||||
className={styles.filePicker}
|
||||
onDismiss={this._handleClosePanel}
|
||||
type={PanelType.extraLarge}
|
||||
isFooterAtBottom={true}
|
||||
onRenderNavigation={() => { return undefined; }}
|
||||
headerText={strings.FilePickerHeader}
|
||||
isLightDismiss={true}
|
||||
onRenderHeader={() => this._renderHeader()}
|
||||
>
|
||||
|
||||
<div className={styles.nav}>
|
||||
<Nav
|
||||
groups={groups}
|
||||
selectedKey={this.state.selectedTab}
|
||||
onLinkClick={(ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => this._handleLinkClick(ev, item)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.tabsContainer}>
|
||||
{this.state.selectedTab === "keyLink" && <LinkFilePickerTab
|
||||
itemType={this.props.itemType}
|
||||
allowExternalTenantLinks={true}
|
||||
accepts={accepts}
|
||||
context={this.props.webPartContext}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
{this.state.selectedTab === "keyUpload" && <UploadFilePickerTab
|
||||
itemType={this.props.itemType}
|
||||
context={this.props.webPartContext}
|
||||
accepts={accepts}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
{this.state.selectedTab === "keySite" && <SiteFilePickerTab
|
||||
itemType={this.props.itemType}
|
||||
context={this.props.webPartContext}
|
||||
accepts={accepts}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
{this.state.selectedTab === "keyWeb" && <WebSearchTab
|
||||
itemType={this.props.itemType}
|
||||
context={this.props.webPartContext}
|
||||
accepts={accepts}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
{this.state.selectedTab === "keyOneDrive" && <OneDriveTab
|
||||
itemType={this.props.itemType}
|
||||
context={this.props.webPartContext}
|
||||
accepts={accepts}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
{this.state.selectedTab === "keyRecent" && <RecentFilesTab
|
||||
itemType={this.props.itemType}
|
||||
context={this.props.webPartContext}
|
||||
accepts={accepts}
|
||||
onClose={() => this._handleClosePanel()}
|
||||
onSave={(value: string) => this._handleSave(value)}
|
||||
/>}
|
||||
|
||||
</div>
|
||||
</Panel>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the panel header
|
||||
*/
|
||||
private _renderHeader = (): JSX.Element => {
|
||||
return <div className={"ms-Panel-header"}><p className={css("ms-Panel-headerText", styles.header)} role="heading">{strings.FilePickerHeader}</p></div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the panel
|
||||
*/
|
||||
private _handleOpenPanel = () => {
|
||||
this.setState({
|
||||
panelOpen: true,
|
||||
selectedTab: 'keyRecent'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the panel
|
||||
*/
|
||||
private _handleClosePanel = () => {
|
||||
this.setState({
|
||||
panelOpen: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On save action
|
||||
*/
|
||||
private _handleSave = (image: string) => {
|
||||
this.props.onChanged(image);
|
||||
this.setState({
|
||||
panelOpen: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the selected tab when a link is selected
|
||||
*/
|
||||
private _handleLinkClick = (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
|
||||
this.setState({ selectedTab: item.key });
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.gridListCell {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .gridListCell {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
[dir="ltr"] .gridListCell.cellRight {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gridListCell.cellLastRow {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.itemTile.isFile,
|
||||
.itemTile.isPhoto,
|
||||
.itemTile.isVideo {
|
||||
background-color: $ms-color-neutralLighter;
|
||||
}
|
||||
|
||||
.itemTile {
|
||||
-webkit-transition: -webkit-transform .1s linear;
|
||||
transition: -webkit-transform .1s linear;
|
||||
transition: transform .1s linear;
|
||||
transition: transform .1s linear,-webkit-transform .1s linear;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
-webkit-transform: scale(0.96);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
border: 1px solid;
|
||||
border-color: $ms-color-white;
|
||||
-webkit-box-shadow: 0 0 0 2px $ms-color-neutralSecondaryAlt;
|
||||
box-shadow: 0 0 0 2px $ms-color-neutralSecondaryAlt;
|
||||
}
|
||||
}
|
||||
|
||||
.hasThumbnail {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.itemTileContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.itemTile.isFile .itemTileDiscoverFileContainer,
|
||||
.itemTile.isFile .itemTileFileContainer {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $ms-color-neutralLight;
|
||||
}
|
||||
|
||||
.itemTileFile {
|
||||
.itemTileFileContainer {
|
||||
position: absolute;
|
||||
background-color: $ms-color-neutralLighter;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
.itemTileThumbnail {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.image {
|
||||
img {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
animation-duration: 0.367s;
|
||||
animation-timing-function: cubic-bezier(0.1, 0.25, 0.75, 0.9);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[dir="ltr"] .itemTileCheckCircle {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.itemTileCheckCircle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
opacity: 0;
|
||||
background: 0 0;
|
||||
border: none;
|
||||
outline: 0;
|
||||
top: 8px;
|
||||
|
||||
:global(.ms-Check-check) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.gridListCell.isSelected .itemTileCheckCircle,
|
||||
.gridListCell:hover,
|
||||
.gridListCell:hover .itemTileCheckCircle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.itemTileNamePlate {
|
||||
position: absolute;
|
||||
bottom:0;
|
||||
left:0;
|
||||
right:0;
|
||||
min-height:20px;
|
||||
padding:4px 8px 8px;
|
||||
background-color:$ms-color-white;
|
||||
color:$ms-color-neutralPrimary;
|
||||
white-space:nowrap;
|
||||
text-overflow:ellipsis
|
||||
}
|
||||
.itemTile.isFile .itemTileNamePlate {
|
||||
opacity: .95;
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-color: $ms-color-neutralLight;
|
||||
min-height: 35px;
|
||||
}
|
||||
|
||||
.itemTileName {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.itemTileChildCount, .itemTileName, .itemTileSubText {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.itemTileSubText {
|
||||
font-size: 11px;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.recentGridList {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.recentGridList :global(.ms-List-page) {
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Custom styles
|
||||
import styles from './RecentFilesTab.module.scss';
|
||||
|
||||
// Office Fabric
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
|
||||
import { List } from 'office-ui-fabric-react/lib/List';
|
||||
import { IRectangle } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { css } from "@uifabric/utilities/lib/css";
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { Selection, SelectionMode, SelectionZone } from 'office-ui-fabric-react/lib/Selection';
|
||||
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import { Check } from 'office-ui-fabric-react/lib/Check';
|
||||
|
||||
// Custom props and states
|
||||
import { IRecentFilesTabProps, IRecentFilesTabState, IRecentFile } from './RecentFilesTab.types';
|
||||
import { ItemType } from '../IPropertyPaneFilePicker';
|
||||
|
||||
// Localized resources
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
// PnP
|
||||
import { sp, SearchResults, SearchResult } from "@pnp/sp";
|
||||
|
||||
/**
|
||||
* Rows per page
|
||||
*/
|
||||
const ROWS_PER_PAGE = 3;
|
||||
|
||||
/**
|
||||
* Maximum row height
|
||||
*/
|
||||
const MAX_ROW_HEIGHT = 250;
|
||||
|
||||
export default class RecentFilesTab extends React.Component<IRecentFilesTabProps, IRecentFilesTabState> {
|
||||
private _columnCount: number;
|
||||
private _columnWidth: number;
|
||||
private _rowHeight: number;
|
||||
private _selection: Selection;
|
||||
private _listElem: List = undefined;
|
||||
|
||||
constructor(props: IRecentFilesTabProps) {
|
||||
super(props);
|
||||
|
||||
this._selection = new Selection(
|
||||
{
|
||||
selectionMode: SelectionMode.single,
|
||||
onSelectionChanged: () => {
|
||||
// Get the selected item
|
||||
const selectedItems = this._selection.getSelection();
|
||||
if (selectedItems && selectedItems.length > 0) {
|
||||
//Get the selected key
|
||||
const selectedKey: IRecentFile = selectedItems[0] as IRecentFile;
|
||||
|
||||
// Save the selected file
|
||||
this.setState({
|
||||
fileUrl: selectedKey.fileUrl
|
||||
});
|
||||
} else {
|
||||
// Remove any selected file
|
||||
this.setState({
|
||||
fileUrl: undefined
|
||||
});
|
||||
}
|
||||
if (this._listElem) {
|
||||
// Force the list to update to show the selection check
|
||||
this._listElem.forceUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recently used files
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
const { absoluteUrl } = this.props.context.pageContext.web;
|
||||
|
||||
// Build a filter criteria for each accepted file type, if applicable
|
||||
const fileFilter: string = this._getFileFilter();
|
||||
|
||||
// // This is how you make two promises at once and wait for both results to return
|
||||
// // TODO: research to see if there is a way to get this info in one call. Perhaps as context info?
|
||||
sp.setup({
|
||||
sp: { baseUrl: absoluteUrl }
|
||||
});
|
||||
const getContext: [Promise<any[]>, Promise<any[]>] = [sp.web.select("Id").get(), sp.site.select("Id").get()];
|
||||
|
||||
|
||||
Promise.all(getContext).then((results: any[]) => {
|
||||
// retrieve site id and web id
|
||||
const webId: string = results[0].Id;
|
||||
const siteId: string = results[1].Id;
|
||||
|
||||
// build a query template
|
||||
const queryTemplate: string = `((SiteID:${siteId} OR SiteID: {${siteId}}) AND (WebId: ${webId} OR WebId: {${webId}})) AND LastModifiedTime < {Today} AND -Title:OneNote_DeletedPages AND -Title:OneNote_RecycleBin${fileFilter}`;
|
||||
|
||||
// search for recent changes with accepted file types
|
||||
sp.search({
|
||||
QueryTemplate: queryTemplate,
|
||||
RowLimit: 20,
|
||||
SelectProperties: [
|
||||
"Title",
|
||||
"Path",
|
||||
"Filename",
|
||||
"FileExtension",
|
||||
"FileType",
|
||||
"Created",
|
||||
"Author",
|
||||
"LastModifiedTime",
|
||||
"EditorOwsUser",
|
||||
"ModifiedBy",
|
||||
"LinkingUrl",
|
||||
"SiteTitle",
|
||||
"ParentLink",
|
||||
"DocumentPreviewMetadata",
|
||||
"ListID",
|
||||
"ListItemID",
|
||||
"SPSiteURL",
|
||||
"SiteID",
|
||||
"WebId",
|
||||
"UniqueID",
|
||||
"SPWebUrl",
|
||||
"DefaultEncodingURL",
|
||||
"PictureThumbnailURL",
|
||||
],
|
||||
SortList: [
|
||||
{
|
||||
"Property": "LastModifiedTime",
|
||||
"Direction": 1
|
||||
}
|
||||
]
|
||||
}).then((r: SearchResults) => {
|
||||
const recentFilesResult: IRecentFile[] = r.PrimarySearchResults.map((result: SearchResult) => {
|
||||
const recentFile: IRecentFile = {
|
||||
key: result["UniqueID"],
|
||||
name: result.Title,
|
||||
fileUrl: result["DefaultEncodingURL"],
|
||||
editedBy: result["ModifiedBy"]
|
||||
};
|
||||
return recentFile;
|
||||
});
|
||||
this._selection.setItems(recentFilesResult, true);
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
results: recentFilesResult
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tab
|
||||
*/
|
||||
public render(): React.ReactElement<IRecentFilesTabProps> {
|
||||
const imageType: boolean = this.props.itemType === ItemType.Images;
|
||||
const { results,
|
||||
isLoading } = this.state;
|
||||
return (
|
||||
<span className={styles.tabContainer}>
|
||||
<span className={styles.tabHeaderContainer}>
|
||||
<h2 className={styles.tabHeader}>{imageType ? strings.RecentImagesHeader : strings.RecentDocumentsHeader}</h2>
|
||||
</span>
|
||||
<span className={styles.tab}>
|
||||
{isLoading ?
|
||||
this._renderSpinner() :
|
||||
results === undefined || results.length < 1 ? this._renderPlaceholder() : this._renderGridList()
|
||||
}
|
||||
</span>
|
||||
<span className={styles.actionButtonsContainer}>
|
||||
<span className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={!this.state.fileUrl}
|
||||
onClick={() => this._handleSave()}
|
||||
className={styles.actionButton}
|
||||
>{strings.OpenButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many items there should be in the page
|
||||
*/
|
||||
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
|
||||
if (itemIndex === 0) {
|
||||
this._columnCount = Math.ceil(surfaceRect.width / MAX_ROW_HEIGHT);
|
||||
this._columnWidth = Math.floor(surfaceRect.width / this._columnCount);
|
||||
this._rowHeight = this._columnWidth;
|
||||
}
|
||||
|
||||
return this._columnCount * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
/** Calculates the list "page" height (a.k.a. row) */
|
||||
private _getPageHeight = (): number => {
|
||||
return this._rowHeight * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders a "please wait" spinner while we're loading
|
||||
*/
|
||||
private _renderSpinner = (): JSX.Element => {
|
||||
return <Spinner label={strings.Loading} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a message saying that there are no recent files
|
||||
*/
|
||||
private _renderPlaceholder = (): JSX.Element => {
|
||||
return <Placeholder iconName='OpenFolderHorizontal'
|
||||
iconText={strings.NoRecentFiles}
|
||||
description={strings.NoRecentFilesDescription}
|
||||
/>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a grid list containing results
|
||||
*/
|
||||
private _renderGridList = (): JSX.Element => {
|
||||
return <span className={styles.recentGridList} role="grid">
|
||||
<FocusZone>
|
||||
<SelectionZone selection={this._selection}
|
||||
onItemInvoked={(item: IRecentFile) => this._handleItemInvoked(item)}>
|
||||
<List
|
||||
ref={this._linkElement}
|
||||
items={this.state.results}
|
||||
onRenderCell={this._onRenderCell}
|
||||
getItemCountForPage={this._getItemCountForPage}
|
||||
getPageHeight={this._getPageHeight}
|
||||
renderedWindowsAhead={4}
|
||||
/>
|
||||
</SelectionZone>
|
||||
</FocusZone>
|
||||
</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders each result in its own cell
|
||||
*/
|
||||
private _onRenderCell = (item: IRecentFile, index: number | undefined): JSX.Element => {
|
||||
let isSelected: boolean = false;
|
||||
|
||||
if (this._selection && index !== undefined) {
|
||||
isSelected = this._selection.isIndexSelected(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.gridListCell} role={"gridCell"}>
|
||||
<div
|
||||
className={css(styles.itemTile, styles.isFile, styles.hasThumbnail, isSelected ? styles.isSelected : undefined)}
|
||||
role="link"
|
||||
aria-selected={isSelected}
|
||||
data-is-draggable="false"
|
||||
data-is-focusable="true"
|
||||
data-selection-index={index}
|
||||
data-selection-invoke="true"
|
||||
data-item-index={index}
|
||||
data-automationid="ItemTile"
|
||||
style={{
|
||||
width: this._columnWidth,
|
||||
height: this._rowHeight
|
||||
}}
|
||||
>
|
||||
<div className={styles.itemTileContent}>
|
||||
<div className={styles.itemTileFile}>
|
||||
<div className={styles.itemTileFileContainer}>
|
||||
<div className={styles.itemTileThumbnail}>
|
||||
{/* <div className={styles.image}> */}
|
||||
<Image src={item.fileUrl} width={this._columnWidth} height={this._rowHeight} imageFit={ImageFit.cover} />
|
||||
{/* </div> */}
|
||||
</div>
|
||||
<div className={styles.itemTileCheckCircle}
|
||||
role='checkbox'
|
||||
aria-checked={isSelected}
|
||||
data-item-index={index} data-selection-toggle={true} data-automationid='CheckCircle'>
|
||||
<Check checked={isSelected} />
|
||||
</div>
|
||||
<div className={styles.itemTileNamePlate}>
|
||||
<div className={styles.itemTileName}>{item.name}</div>
|
||||
<div className={styles.itemTileSubText}>
|
||||
<span>{strings.EditedByNamePlate}{item.editedBy}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called what a file is selected.
|
||||
*/
|
||||
private _handleItemInvoked = (item: IRecentFile) => {
|
||||
this._selection.setKeySelected(item.key, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when it is time to save the currently selected item
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when it is time to close (without saving)
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a file filter using the accepted file extensions
|
||||
*/
|
||||
private _getFileFilter() {
|
||||
let fileFilter: string = undefined;
|
||||
if (this.props.itemType === ItemType.Images && this.props.accepts) {
|
||||
fileFilter = " AND (";
|
||||
this.props.accepts.split(",").forEach((fileType: string, index: number) => {
|
||||
fileType = fileType.replace(".", "");
|
||||
if (index > 0) {
|
||||
fileFilter = fileFilter + " OR ";
|
||||
}
|
||||
fileFilter = fileFilter + `FileExtension:${fileType} OR SecondaryFileExtension:${fileType}`;
|
||||
});
|
||||
fileFilter = fileFilter + ")";
|
||||
}
|
||||
return fileFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ref to the list
|
||||
*/
|
||||
private _linkElement = (e: any) => {
|
||||
this._listElem = e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
|
||||
export interface IRecentFilesTabProps extends IFilePickerTab {
|
||||
//inherited
|
||||
}
|
||||
|
||||
export interface IRecentFilesTabState {
|
||||
results: IRecentFile[];
|
||||
isLoading: boolean;
|
||||
fileUrl?: string;
|
||||
}
|
||||
export interface IRecentFile {
|
||||
fileUrl: string;
|
||||
key: string;
|
||||
name: string;
|
||||
editedBy: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './RecentFilesTab';
|
||||
export * from './RecentFilesTab.types';
|
|
@ -0,0 +1,267 @@
|
|||
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
|
||||
//@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.aboveNameplate {
|
||||
position: relative;
|
||||
margin: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.tileContent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
// -webkit-box-orient: vertical;
|
||||
// -webkit-box-direction: normal;
|
||||
// -ms-flex-direction: column;
|
||||
// flex-direction: column;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.foreground {
|
||||
display: -webkit-inline-box;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.folderCover {
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.folderCover.isLarge {
|
||||
width: 112px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.folderCoverBack {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.folderCoverFront {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
right: -3px;
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.listPage {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
// -webkit-box-orient: vertical;
|
||||
// -webkit-box-direction: normal;
|
||||
// -ms-flex-direction: column;
|
||||
// flex-direction: column;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
-webkit-box-align: start;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
-ms-flex-line-pack: start;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.cellContent {
|
||||
//position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tile {
|
||||
outline: transparent;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
background-color: $ms-color-white;
|
||||
-webkit-transition: -webkit-transform 0.1s linear;
|
||||
transition: -webkit-transform 0.1s linear;
|
||||
transition: transform 0.1s linear;
|
||||
transition: transform 0.1s linear, -webkit-transform 0.1s linear;
|
||||
color: $ms-color-neutralPrimary;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.namePlate {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
font-size: $ms-font-size-s;
|
||||
box-sizing: border-box;
|
||||
margin: auto 0 0 0;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.nameplate .name {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .name {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
html[dir="ltr"] .name {
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.name,
|
||||
.isLarge .name {
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
margin-top: -7px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.nameplate .name {
|
||||
-webkit-transition: color 0.2s linear;
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
color: $ms-color-neutralPrimary;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.link:hover .name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.folderList {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.folderList :global(.ms-List-cell) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.folderList :global(.ms-List-page) {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
-webkit-box-direction: normal;
|
||||
-webkit-box-orient: horizontal;
|
||||
}
|
||||
|
||||
.folderList :global(.ms-List-surface) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
background-color: $ms-color-white;
|
||||
outline: transparent;
|
||||
}
|
||||
|
||||
.cell {
|
||||
//padding-top: 97.16%;
|
||||
padding-top: 140px;
|
||||
}
|
||||
|
||||
.listCell {
|
||||
text-align: center;
|
||||
outline: none;
|
||||
position: relative;
|
||||
max-width: 176px;
|
||||
max-height: 171px;
|
||||
margin: 4px;
|
||||
border-style: none;
|
||||
border-width: 0px;
|
||||
width: 176px;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import * as React from 'react';
|
||||
import styles from './DocumentLibraryBrowser.module.scss';
|
||||
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
|
||||
import { List } from 'office-ui-fabric-react/lib/List';
|
||||
import { css } from "@uifabric/utilities/lib/css";
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
|
||||
import { IDocumentLibraryBrowserProps, IDocumentLibraryBrowserState, ILibrary } from './DocumentLibraryBrowser.types';
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
/**
|
||||
* This would have been better done as an Office Fabric TileList, but it isn't available yet for production use
|
||||
*/
|
||||
export default class DocumentLibraryBrowser extends React.Component<IDocumentLibraryBrowserProps, IDocumentLibraryBrowserState> {
|
||||
constructor(props: IDocumentLibraryBrowserProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
lists: []
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
|
||||
const { absoluteUrl } = this.props.context.pageContext.web;
|
||||
|
||||
const apiUrl: string = `${absoluteUrl}/_api/SP.Web.GetDocumentAndMediaLibraries?webFullUrl='${encodeURIComponent(absoluteUrl)}'&includePageLibraries='false'`;
|
||||
this.props.context.spHttpClient.get(apiUrl,
|
||||
SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
response.json().then((responseJSON: any) => {
|
||||
const lists: ILibrary[] = responseJSON.value.map((item) => {
|
||||
const list: ILibrary = {
|
||||
title: item.Title,
|
||||
absoluteUrl: item.AbsoluteUrl,
|
||||
serverRelativeUrl: item.ServerRelativeUrl
|
||||
};
|
||||
return list;
|
||||
});
|
||||
this.setState({
|
||||
lists: lists,
|
||||
isLoading: false
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IDocumentLibraryBrowserProps> {
|
||||
if (this.state.isLoading) {
|
||||
return (<Spinner label={strings.Loading} />);
|
||||
}
|
||||
return (
|
||||
<FocusZone>
|
||||
<List
|
||||
className={styles.folderList}
|
||||
items={this.state.lists}
|
||||
onRenderCell={this._onRenderCell}
|
||||
/>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a file folder cover
|
||||
*/
|
||||
private _onRenderCell = (item: ILibrary, index: number | undefined): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={styles.listCell}
|
||||
data-is-focusable={true}
|
||||
>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellContent}>
|
||||
<a className={styles.link} onClick={(_event) => this._handleOpenLibrary(item)} >
|
||||
<span className={styles.aboveNameplate}>
|
||||
<span className={styles.content}>
|
||||
<div className={css(styles.folderCover, styles.isLarge)}>
|
||||
<img src={strings.FolderBackPlate} className={styles.folderCoverBack}></img>
|
||||
<img src={strings.FolderFrontPlate} className={styles.folderCoverFront} />
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.namePlate}><span className={styles.name}>{item.title}</span></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls parent when library is opened
|
||||
*/
|
||||
private _handleOpenLibrary = (library: ILibrary) => {
|
||||
this.props.onOpenLibrary(library);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
|
||||
export interface IDocumentLibraryBrowserProps {
|
||||
context: WebPartContext;
|
||||
onOpenLibrary: (selectedLibrary: ILibrary) => void;
|
||||
}
|
||||
|
||||
export interface IDocumentLibraryBrowserState {
|
||||
isLoading: boolean;
|
||||
lists: ILibrary[];
|
||||
}
|
||||
|
||||
export interface ILibrary {
|
||||
title: string;
|
||||
absoluteUrl: string;
|
||||
serverRelativeUrl: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './DocumentLibraryBrowser';
|
||||
export * from './DocumentLibraryBrowser.types';
|
|
@ -0,0 +1,3 @@
|
|||
//@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
|
||||
@import "../SiteFilePickerTab.module.scss";
|
||||
|
|
@ -0,0 +1,510 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Custom styles
|
||||
import styles from './FileBrowser.module.scss';
|
||||
|
||||
// Custom properties and state
|
||||
import { IFileBrowserProps, IFileBrowserState, IFile, ViewType } from './FileBrowser.types';
|
||||
|
||||
// PnP library for navigating through libraries
|
||||
import { sp, RenderListDataParameters, RenderListDataOptions } from "@pnp/sp";
|
||||
|
||||
// Office Fabric
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
IColumn,
|
||||
IDetailsRowProps,
|
||||
DetailsRow
|
||||
} from 'office-ui-fabric-react/lib/DetailsList';
|
||||
import { CommandBar, ICommandBarItemProps } from 'office-ui-fabric-react/lib/CommandBar';
|
||||
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
|
||||
|
||||
const LAYOUT_STORAGE_KEY: string = 'comparerSiteFilesLayout';
|
||||
|
||||
// Localized strings
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
// OneDrive services
|
||||
import { OneDriveServices } from '../../../../services/OneDriveServices';
|
||||
import { FormatBytes, GetAbsoluteDomainUrl } from '../../../../CommonUtils';
|
||||
|
||||
/**
|
||||
* Renders list of file in a list.
|
||||
* I should have used the PnP ListView control, but I wanted specific behaviour that I didn't
|
||||
* get with the PnP control.
|
||||
*/
|
||||
export default class FileBrowser extends React.Component<IFileBrowserProps, IFileBrowserState> {
|
||||
private _selection: Selection;
|
||||
|
||||
constructor(props: IFileBrowserProps) {
|
||||
super(props);
|
||||
|
||||
// If possible, load the user's favourite layout
|
||||
const lastLayout: ViewType = localStorage ?
|
||||
localStorage.getItem(LAYOUT_STORAGE_KEY) as ViewType
|
||||
: 'list' as ViewType;
|
||||
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: 'column1',
|
||||
name: 'Type',
|
||||
ariaLabel: strings.TypeAriaLabel,
|
||||
iconName: 'Page',
|
||||
isIconOnly: true,
|
||||
fieldName: 'docIcon',
|
||||
headerClassName: styles.iconColumnHeader,
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IFile) => {
|
||||
const folderIcon: string = strings.FolderIconUrl;
|
||||
const iconUrl: string = strings.PhotoIconUrl;
|
||||
const altText: string = item.isFolder ? strings.FolderAltText : strings.ImageAltText.replace('{0}', item.fileType);
|
||||
return <div className={styles.fileTypeIcon}>
|
||||
<img src={item.isFolder ? folderIcon : iconUrl} className={styles.fileTypeIconIcon} alt={altText} title={altText} />
|
||||
</div>;
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'column2',
|
||||
name: strings.NameField,
|
||||
fieldName: 'fileLeafRef',
|
||||
minWidth: 210,
|
||||
isRowHeader: true,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
sortAscendingAriaLabel: strings.SortedAscending,
|
||||
sortDescendingAriaLabel: strings.SortedDescending,
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'string',
|
||||
isPadded: true,
|
||||
onRender: (item: IFile) => {
|
||||
if (item.isFolder) {
|
||||
return <span className={styles.folderItem} onClick={(_event) => this._handleOpenFolder(item)}>{item.fileLeafRef}</span>;
|
||||
} else {
|
||||
return <span className={styles.fileItem}>{item.fileLeafRef}</span>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'column3',
|
||||
name: strings.ModifiedField,
|
||||
fieldName: 'dateModifiedValue',
|
||||
minWidth: 120,
|
||||
isResizable: true,
|
||||
onColumnClick: this._onColumnClick,
|
||||
data: 'number',
|
||||
onRender: (item: IFile) => {
|
||||
//const dateModified = moment(item.modified).format(strings.DateFormat);
|
||||
return <span>{item.modified}</span>;
|
||||
},
|
||||
isPadded: true
|
||||
},
|
||||
{
|
||||
key: 'column4',
|
||||
name: strings.ModifiedByField,
|
||||
fieldName: 'modifiedBy',
|
||||
minWidth: 120,
|
||||
isResizable: true,
|
||||
data: 'string',
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IFile) => {
|
||||
return <span>{item.modifiedBy}</span>;
|
||||
},
|
||||
isPadded: true
|
||||
},
|
||||
{
|
||||
key: 'column5',
|
||||
name: strings.FileSizeField,
|
||||
fieldName: 'fileSizeRaw',
|
||||
minWidth: 70,
|
||||
maxWidth: 90,
|
||||
isResizable: true,
|
||||
data: 'number',
|
||||
onColumnClick: this._onColumnClick,
|
||||
onRender: (item: IFile) => {
|
||||
return <span>{item.fileSize ? FormatBytes(item.fileSize, 1) : undefined}</span>;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
this._selection = new Selection({
|
||||
onSelectionChanged: () => {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this.state = {
|
||||
columns: columns,
|
||||
items: [],
|
||||
isLoading: true,
|
||||
currentPath: this.props.rootPath,
|
||||
selectedView: lastLayout
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of files when settings change
|
||||
* @param prevProps
|
||||
* @param prevState
|
||||
*/
|
||||
public componentDidUpdate(prevProps: IFileBrowserProps, prevState: IFileBrowserState): void {
|
||||
|
||||
if (prevState.currentPath !== prevState.currentPath) {
|
||||
this._getListItems();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of files when tab first loads
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
this._getListItems();
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IFileBrowserProps> {
|
||||
if (this.state.isLoading) {
|
||||
return (<Spinner label={strings.Loading} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.itemPickerTopBar}>
|
||||
<CommandBar
|
||||
items={this._getToolbarItems()}
|
||||
farItems={this.getFarItems()}
|
||||
/>
|
||||
</div>
|
||||
<DetailsList
|
||||
items={this.state.items}
|
||||
compact={this.state.selectedView === 'compact'}
|
||||
columns={this.state.columns}
|
||||
selectionMode={SelectionMode.single}
|
||||
setKey="set"
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
isHeaderVisible={true}
|
||||
selection={this._selection}
|
||||
selectionPreservedOnEmptyClick={true}
|
||||
onActiveItemChanged={(item: IFile, index: number, ev: React.FormEvent<Element>) => this._itemChangedHandler(item, index, ev)}
|
||||
enterModalSelectionOnTouch={true}
|
||||
onRenderRow={this._onRenderRow}
|
||||
/>
|
||||
{this.state.items === undefined || this.state.items.length < 1 &&
|
||||
this._renderEmptyFolder()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a placeholder to indicate that the folder is empty
|
||||
*/
|
||||
private _renderEmptyFolder = (): JSX.Element => {
|
||||
return (<div className={styles.emptyFolder}>
|
||||
<div className={styles.emptyFolderImage}>
|
||||
<img
|
||||
className={styles.emptyFolderImageTag}
|
||||
src={strings.OneDriveEmptyFolderIconUrl}
|
||||
alt={strings.OneDriveEmptyFolderAlt} />
|
||||
</div>
|
||||
<div role="alert">
|
||||
<div className={styles.emptyFolderTitle}>
|
||||
{strings.OneDriveEmptyFolderTitle}
|
||||
</div>
|
||||
<div className={styles.emptyFolderSubText}>
|
||||
<span className={styles.emptyFolderPc}>
|
||||
{strings.OneDriveEmptyFolderDescription}
|
||||
</span>
|
||||
{/* Removed until we add support to upload */}
|
||||
{/* <span className={styles.emptyFolderMobile}>
|
||||
Tap <Icon iconName="Add" className={styles.emptyFolderIcon} /> to add files here.
|
||||
</span> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
private _onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
const fileItem: IFile = props.item;
|
||||
|
||||
return <DetailsRow {...props} className={fileItem.isFolder ? styles.folderRow : styles.fileRow} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of toolbar items on the left side of the toolbar.
|
||||
* We leave it empty for now, but we may add the ability to upload later.
|
||||
*/
|
||||
private _getToolbarItems = (): ICommandBarItemProps[] => {
|
||||
return [
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
private getFarItems = (): ICommandBarItemProps[] => {
|
||||
const { selectedView } = this.state;
|
||||
|
||||
let viewIconName: string = undefined;
|
||||
let viewName: string = undefined;
|
||||
switch (this.state.selectedView) {
|
||||
case 'list':
|
||||
viewIconName = 'List';
|
||||
viewName = strings.ListLayoutList;
|
||||
break;
|
||||
case 'compact':
|
||||
viewIconName = 'AlignLeft';
|
||||
viewName = strings.ListLayoutCompact;
|
||||
break;
|
||||
default:
|
||||
viewIconName = 'GridViewMedium';
|
||||
viewName = strings.ListLayoutTile;
|
||||
}
|
||||
|
||||
const farItems: ICommandBarItemProps[] = [
|
||||
{
|
||||
key: 'listOptions',
|
||||
className: styles.commandBarNoChevron,
|
||||
title: strings.ListOptionsTitle,
|
||||
ariaLabel: strings.ListOptionsAlt.replace('{0}', viewName),
|
||||
iconProps: {
|
||||
iconName: viewIconName
|
||||
},
|
||||
iconOnly: true,
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: 'list',
|
||||
name: strings.ListLayoutList,
|
||||
iconProps: {
|
||||
iconName: 'List'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'list',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutList).replace('{1}', selectedView === 'list' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutListDescrition,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
},
|
||||
{
|
||||
key: 'compact',
|
||||
name: strings.ListLayoutCompact,
|
||||
iconProps: {
|
||||
iconName: 'AlignLeft'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'compact',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutCompact).replace('{1}', selectedView === 'compact' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutCompactDescription,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
},
|
||||
{
|
||||
key: 'tiles',
|
||||
name: 'Tiles',
|
||||
iconProps: {
|
||||
iconName: 'GridViewMedium'
|
||||
},
|
||||
canCheck: true,
|
||||
checked: this.state.selectedView === 'tiles',
|
||||
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutTile).replace('{1}', selectedView === 'tiles' ? strings.Selected : undefined),
|
||||
title: strings.ListLayoutTileDescription,
|
||||
onClick: (_ev?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, item?: IContextualMenuItem) => this._handleSwitchLayout(item)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
return farItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when users switch the view
|
||||
*/
|
||||
private _handleSwitchLayout = (item?: IContextualMenuItem) => {
|
||||
if (item) {
|
||||
// Store the user's favourite layout
|
||||
if (localStorage) {
|
||||
localStorage.setItem(LAYOUT_STORAGE_KEY, item.key);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedView: item.key as ViewType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gratuitous sorting
|
||||
*/
|
||||
private _onColumnClick = (event: React.MouseEvent<HTMLElement>, column: IColumn): void => {
|
||||
const { columns } = this.state;
|
||||
let { items } = this.state;
|
||||
let isSortedDescending = column.isSortedDescending;
|
||||
|
||||
// If we've sorted this column, flip it.
|
||||
if (column.isSorted) {
|
||||
isSortedDescending = !isSortedDescending;
|
||||
}
|
||||
|
||||
// Sort the items.
|
||||
items = items!.concat([]).sort((a, b) => {
|
||||
const firstValue = a[column.fieldName || ''];
|
||||
const secondValue = b[column.fieldName || ''];
|
||||
|
||||
if (isSortedDescending) {
|
||||
return firstValue > secondValue ? -1 : 1;
|
||||
} else {
|
||||
return firstValue > secondValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset the items and columns to match the state.
|
||||
this.setState({
|
||||
items: items,
|
||||
columns: columns!.map(col => {
|
||||
col.isSorted = col.key === column.key;
|
||||
|
||||
if (col.isSorted) {
|
||||
col.isSortedDescending = isSortedDescending;
|
||||
}
|
||||
|
||||
return col;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When a folder is opened, calls parent tab to navigate down
|
||||
*/
|
||||
private _handleOpenFolder = (item: IFile) => {
|
||||
// De-select the list item that was clicked, the item in the same position
|
||||
// item in the folder will appear selected
|
||||
this.setState({
|
||||
fileUrl: undefined,
|
||||
currentPath: item.fileRef
|
||||
}, () => this._getListItems());
|
||||
this.props.onOpenFolder(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* When user selects an item, save selection
|
||||
*/
|
||||
private _itemChangedHandler = (item: IFile, _index: number, _ev): void => {
|
||||
if (item.isFolder) {
|
||||
this.setState({
|
||||
fileUrl: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify parent tab
|
||||
const absoluteFileUrl: string = item.absoluteRef;
|
||||
this.props.onChange(absoluteFileUrl);
|
||||
this.setState({
|
||||
fileUrl: absoluteFileUrl
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all files in a library with a matchihg path
|
||||
*/
|
||||
private _getListItems() {
|
||||
this.setState({
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
const fileFilter: string = OneDriveServices.GetFileTypeFilter(this.props.accepts);
|
||||
|
||||
const parms: RenderListDataParameters = {
|
||||
RenderOptions: RenderListDataOptions.ContextInfo | RenderListDataOptions.ListData | RenderListDataOptions.ListSchema | RenderListDataOptions.ViewMetadata | RenderListDataOptions.EnableMediaTAUrls | RenderListDataOptions.ParentInfo,//4231, //4103, //4231, //192, //64
|
||||
AllowMultipleValueFilterForTaxonomyFields: true,
|
||||
FolderServerRelativeUrl: this.state.currentPath,
|
||||
ViewXml:
|
||||
`<View>
|
||||
<Query>
|
||||
<Where>
|
||||
<Or>
|
||||
<And>
|
||||
<Eq>
|
||||
<FieldRef Name="FSObjType" />
|
||||
<Value Type="Text">1</Value>
|
||||
</Eq>
|
||||
<Eq>
|
||||
<FieldRef Name="SortBehavior" />
|
||||
<Value Type="Text">1</Value>
|
||||
</Eq>
|
||||
</And>
|
||||
<In>
|
||||
<FieldRef Name="File_x0020_Type" />
|
||||
${fileFilter}
|
||||
</In>
|
||||
</Or>
|
||||
</Where>
|
||||
</Query>
|
||||
<ViewFields>
|
||||
<FieldRef Name="DocIcon"/>
|
||||
<FieldRef Name="LinkFilename"/>
|
||||
<FieldRef Name="Modified"/>
|
||||
<FieldRef Name="Editor"/>
|
||||
<FieldRef Name="FileSizeDisplay"/>
|
||||
<FieldRef Name="SharedWith"/>
|
||||
<FieldRef Name="MediaServiceFastMetadata"/>
|
||||
<FieldRef Name="MediaServiceOCR"/>
|
||||
<FieldRef Name="_ip_UnifiedCompliancePolicyUIAction"/>
|
||||
<FieldRef Name="ItemChildCount"/>
|
||||
<FieldRef Name="FolderChildCount"/>
|
||||
<FieldRef Name="SMTotalFileCount"/>
|
||||
<FieldRef Name="SMTotalSize"/>
|
||||
</ViewFields>
|
||||
<RowLimit Paged="TRUE">100</RowLimit>
|
||||
</View>`
|
||||
};
|
||||
|
||||
sp.web.lists.getByTitle(this.props.libraryName).renderListDataAsStream(parms).then((value: any) => {
|
||||
const fileItems: IFile[] = value.ListData.Row.map(fileItem => {
|
||||
const modifiedFriendly: string = fileItem["Modified.FriendlyDisplay"];
|
||||
|
||||
// Get the modified date
|
||||
const modifiedParts: string[] = modifiedFriendly!.split('|');
|
||||
let modified: string = fileItem.Modified;
|
||||
|
||||
// If there is a friendly modified date, use that
|
||||
if (modifiedParts.length === 2) {
|
||||
modified = modifiedParts[1];
|
||||
}
|
||||
|
||||
const file: IFile = {
|
||||
fileLeafRef: fileItem.FileLeafRef,
|
||||
docIcon: fileItem.DocIcon,
|
||||
fileRef: fileItem.FileRef,
|
||||
modified: modified,
|
||||
fileSize: fileItem.File_x0020_Size,
|
||||
fileType: fileItem.File_x0020_Type,
|
||||
modifiedBy: fileItem.Editor![0]!.title,
|
||||
isFolder: fileItem.FSObjType === "1",
|
||||
absoluteRef: this._buildAbsoluteUrl(fileItem.FileRef)
|
||||
};
|
||||
return file;
|
||||
});
|
||||
|
||||
// de-select anything that was previously selected
|
||||
this._selection.setAllSelected(false);
|
||||
this.setState({
|
||||
items: fileItems,
|
||||
isLoading: false
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an absolute URL
|
||||
*/
|
||||
private _buildAbsoluteUrl = (relativeUrl: string) => {
|
||||
const siteUrl: string = GetAbsoluteDomainUrl(this.props.context.pageContext.web.absoluteUrl);
|
||||
return siteUrl + relativeUrl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { IColumn } from 'office-ui-fabric-react/lib/DetailsList';
|
||||
|
||||
export interface IFileBrowserProps {
|
||||
libraryName: string;
|
||||
rootPath: string;
|
||||
accepts: string;
|
||||
context: WebPartContext;
|
||||
onChange: (imageUrl: string) => void;
|
||||
onOpenFolder: (folder: IFile) => void;
|
||||
}
|
||||
|
||||
export interface IFileBrowserState {
|
||||
isLoading: boolean;
|
||||
items: IFile[];
|
||||
currentPath: string;
|
||||
fileUrl?: string;
|
||||
columns: IColumn[];
|
||||
selectedView: ViewType;
|
||||
}
|
||||
|
||||
export interface IFile {
|
||||
docIcon: string;
|
||||
fileRef: string;
|
||||
fileLeafRef: string;
|
||||
modifiedBy?: string;
|
||||
modified: string;
|
||||
fileType?: string;
|
||||
fileSize?: number;
|
||||
isFolder: boolean;
|
||||
absoluteRef: string;
|
||||
}
|
||||
|
||||
export type ViewType = 'list' | 'compact' | 'tiles';
|
|
@ -0,0 +1,2 @@
|
|||
export * from './FileBrowser';
|
||||
export * from './FileBrowser.types';
|
|
@ -0,0 +1,2 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Custom styles
|
||||
import styles from './SiteFilePickerTab.module.scss';
|
||||
|
||||
// Custom picker interface
|
||||
import { ISiteFilePickerTabProps, ISiteFilePickerTabState } from '.';
|
||||
import { ILibrary } from './DocumentLibraryBrowser/DocumentLibraryBrowser.types';
|
||||
import FileBrowser from './FileBrowser/FileBrowser';
|
||||
import { IFile } from './FileBrowser/FileBrowser.types';
|
||||
import DocumentLibraryBrowser from './DocumentLibraryBrowser/DocumentLibraryBrowser';
|
||||
|
||||
// Office Fabric
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
|
||||
// Localized strings
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
export default class SiteFilePickerTab extends React.Component<ISiteFilePickerTabProps, ISiteFilePickerTabState> {
|
||||
constructor(props: ISiteFilePickerTabProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
libraryAbsolutePath: undefined,
|
||||
libraryTitle: strings.DocumentLibraries,
|
||||
libraryPath: undefined,
|
||||
folderName: strings.DocumentLibraries
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<ISiteFilePickerTabProps> {
|
||||
return (
|
||||
<div className={styles.tabContainer}>
|
||||
<div className={styles.tabHeaderContainer}>
|
||||
<h2 className={styles.tabHeader}>{this.state.folderName}</h2>
|
||||
</div>
|
||||
<div className={styles.tab}>
|
||||
{this.state.libraryAbsolutePath === undefined &&
|
||||
<DocumentLibraryBrowser
|
||||
context={this.props.context}
|
||||
onOpenLibrary={(selectedLibrary: ILibrary) => this._handleOpenLibrary(selectedLibrary)} />}
|
||||
{this.state.libraryAbsolutePath !== undefined &&
|
||||
<FileBrowser
|
||||
onChange={(fileUrl: string) => this._handleSelectionChange(fileUrl)}
|
||||
onOpenFolder={(folder: IFile) => this._handleOpenFolder(folder)}
|
||||
context={this.props.context}
|
||||
libraryName={this.state.libraryTitle}
|
||||
rootPath={this.state.libraryPath}
|
||||
accepts={this.props.accepts} />}
|
||||
</div>
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
<div className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={!this.state.fileUrl}
|
||||
onClick={() => this._handleSave()} className={styles.actionButton}>{strings.OpenButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called when user selects a different file
|
||||
*/
|
||||
private _handleSelectionChange = (imageUrl: string) => {
|
||||
this.setState({
|
||||
fileUrl: imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user saves
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user closes tab
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user opens a file folder
|
||||
*/
|
||||
private _handleOpenFolder = (folder: IFile) => {
|
||||
this.setState({
|
||||
libraryPath: folder.fileRef,
|
||||
folderName: folder.fileLeafRef,
|
||||
libraryAbsolutePath: folder.absoluteRef
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when user opens a top-level document library
|
||||
*/
|
||||
private _handleOpenLibrary = (library: ILibrary) => {
|
||||
this.setState({
|
||||
libraryAbsolutePath: library.absoluteUrl,
|
||||
libraryTitle: library.title,
|
||||
libraryPath: library.serverRelativeUrl
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
|
||||
export interface ISiteFilePickerTabProps extends IFilePickerTab {
|
||||
//inherited
|
||||
}
|
||||
|
||||
export interface ISiteFilePickerTabState {
|
||||
fileUrl?: string;
|
||||
libraryAbsolutePath: string;
|
||||
libraryTitle: string;
|
||||
libraryPath: string;
|
||||
folderName: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './SiteFilePickerTab';
|
||||
export * from './SiteFilePickerTab.types';
|
|
@ -0,0 +1,26 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.localTabSinglePreview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.localTabSinglePreviewImage {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 220px;
|
||||
image-orientation: from-image;
|
||||
}
|
||||
|
||||
.localTabLabel {
|
||||
color: $ms-color-themePrimary;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.localTabInput {
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Makes thingy pretty
|
||||
import styles from './UploadFilePickerTab.module.scss';
|
||||
|
||||
// Needed for our custom pane tab
|
||||
import { IUploadFilePickerTabProps, IUploadFilePickerTabState } from '.';
|
||||
import { ItemType } from '../IPropertyPaneFilePicker';
|
||||
|
||||
// Office Fabric to the rescue!
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
|
||||
// Localization
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
export default class UploadFilePickerTab extends React.Component<IUploadFilePickerTabProps, IUploadFilePickerTabState> {
|
||||
constructor(props: IUploadFilePickerTabProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fileUrl: undefined,
|
||||
fileName: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IUploadFilePickerTabProps> {
|
||||
const { fileUrl, fileName } = this.state;
|
||||
const imageType: boolean = this.props.itemType === ItemType.Images;
|
||||
return (
|
||||
<div className={styles.tabContainer}>
|
||||
<div className={styles.tabHeaderContainer}>
|
||||
<h2 className={styles.tabHeader}>{imageType ? strings.UploadImageHeader : strings.UploadFileHeader}</h2>
|
||||
</div>
|
||||
<div className={styles.tab}>
|
||||
<input
|
||||
className={styles.localTabInput}
|
||||
type="file" id="fileInput"
|
||||
accept={this.props.accepts} multiple={false} onChange={(event: React.ChangeEvent<HTMLInputElement>) => this._handleFileUpload(event)} />
|
||||
{fileUrl && <div className={styles.localTabSinglePreview}>
|
||||
<img className={styles.localTabSinglePreviewImage} src={fileUrl} alt={fileName} />
|
||||
</div>}
|
||||
<label className={styles.localTabLabel} htmlFor="fileInput">{
|
||||
imageType ?
|
||||
(fileUrl ? strings.ChangeImageLinkLabel : strings.ChooseImageLinkLabel) :
|
||||
(fileUrl ? strings.ChangeFileLinkLabel : strings.ChooseFileLinkLabel)
|
||||
}</label>
|
||||
</div>
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
<div className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={fileUrl === undefined}
|
||||
onClick={() => this._handleSave()} className={styles.actionButton}>{imageType ? strings.AddImageButtonLabel : strings.AddFileButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when a file is uploaded
|
||||
*/
|
||||
private _handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.files || event.target.files.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the files that were uploaded
|
||||
let files = event.target.files;
|
||||
|
||||
// Grab the first file -- there should always only be one
|
||||
const file = files[0];
|
||||
|
||||
// Convert to base64 image
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
this.setState({
|
||||
fileUrl: reader.result as string,
|
||||
fileName: file.name
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves base64 encoded image back to property pane file picker
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes tab without saving
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
|
||||
export interface IUploadFilePickerTabProps extends IFilePickerTab {
|
||||
// inherited from the base interface
|
||||
}
|
||||
|
||||
export interface IUploadFilePickerTabState {
|
||||
fileUrl?: string;
|
||||
fileName?: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './UploadFilePickerTab';
|
||||
export * from './UploadFilePickerTab.types';
|
|
@ -0,0 +1,42 @@
|
|||
interface IBingSearchResult {
|
||||
webSearchUrl: string;
|
||||
webSearchUrlPingSuffix: string;
|
||||
name: string;
|
||||
thumbnailUrl: string;
|
||||
datePublished: string;
|
||||
isFamilyFriendly: boolean;
|
||||
creativeCommons: string;
|
||||
contentUrl: string;
|
||||
contentUrlPingSuffix: string;
|
||||
hostPageUrl: string;
|
||||
hostPageUrlPingSuffix: string;
|
||||
contentSize: string;
|
||||
encodingFormat: string;
|
||||
hostPageDisplayUrl: string;
|
||||
width: number;
|
||||
height: number;
|
||||
thumbnail: Thumbnail;
|
||||
imageInsightsToken: string;
|
||||
insightsMetadata: InsightsMetadata;
|
||||
imageId: string;
|
||||
accentColor: string;
|
||||
}
|
||||
|
||||
interface InsightsMetadata {
|
||||
pagesIncludingCount: number;
|
||||
availableSizesCount: number;
|
||||
recipeSourcesCount?: number;
|
||||
bestRepresentativeQuery?: BestRepresentativeQuery;
|
||||
}
|
||||
|
||||
interface BestRepresentativeQuery {
|
||||
text: string;
|
||||
displayText: string;
|
||||
webSearchUrl: string;
|
||||
webSearchUrlPingSuffix: string;
|
||||
}
|
||||
|
||||
interface Thumbnail {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
@import "../PropertyPaneFilePicker.module.scss";
|
||||
|
||||
.searchBoxContainer {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 37px;
|
||||
|
||||
.searchBoxMedium {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
|
||||
:global(.ms-Dropdown-title) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.filterDropdown {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[dir="ltr"] .searchBoxContainer {
|
||||
margin-left: 33px;
|
||||
|
||||
.searchBox {
|
||||
margin-right: 35px;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.filePickerFolderCardGrid {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
|
||||
:global {
|
||||
.ms-List-page {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.ms-List-surface {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filePickerFolderCardTile {
|
||||
text-align: center;
|
||||
outline: none;
|
||||
position: relative;
|
||||
@include ms-float(left);
|
||||
}
|
||||
|
||||
.filePickerFolderCardPadder {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
:global(.ms-Fabric--isFocusVisible) .filePickerFolderCardTile:focus:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid $ms-color-white;
|
||||
}
|
||||
|
||||
.filePickerFolderCardSizer {
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
|
||||
.filePickerFolderCardImage {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
color: $ms-color-white;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filePickerFolderCardLabel {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
color: $ms-color-white;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
font-size: 12px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-size: 21px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: $ms-color-neutralLight;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $ms-color-neutralTertiaryAlt;
|
||||
color: $ms-color-neutralDark;
|
||||
}
|
||||
}
|
||||
|
||||
[dir="ltr"] .bingGridListCell {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.bingGridListCell {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bingTile {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
-webkit-transition: -webkit-transform 0.1s linear;
|
||||
transition: -webkit-transform 0.1s linear;
|
||||
transition: transform 0.1s linear;
|
||||
transition: transform 0.1s linear, -webkit-transform 0.1s linear;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
-webkit-transform: scale(0.96);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
border: 1px solid;
|
||||
border-color: $ms-color-white;
|
||||
-webkit-box-shadow: 0 0 0 2px $ms-color-neutralSecondaryAlt;
|
||||
box-shadow: 0 0 0 2px $ms-color-neutralSecondaryAlt;
|
||||
}
|
||||
}
|
||||
|
||||
.bingTileContent {
|
||||
border: 1px solid;
|
||||
border-color: $ms-color-white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bingTileFrame {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
-webkit-box-shadow: 0 0 0 2px $ms-color-neutralTertiaryAlt;
|
||||
box-shadow: 0 0 0 2px $ms-color-neutralTertiaryAlt;
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
[dir="ltr"] .bingTileCheckCircle {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.bingTileCheckCircle {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
opacity: 0;
|
||||
background: 0 0;
|
||||
border: none;
|
||||
outline: 0;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.bingTileNamePlate {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px 8px 8px;
|
||||
background-color: $ms-color-white;
|
||||
color: $ms-color-neutralPrimary;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.95;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bingTile.isSelected .bingTileCheckCircle,
|
||||
.bingTile:hover,
|
||||
.bingTile:hover .bingTileCheckCircle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[dir="ltr"] .copyrightLabel {
|
||||
padding-left: 32px;
|
||||
}
|
||||
.copyrightLabel {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.noResultLabel {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.bingTileThumbnail {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation-duration: 0.367s;
|
||||
animation-timing-function: cubic-bezier(0.1, 0.25, 0.75, 0.9);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
}
|
||||
|
||||
.bingGrildList :global(.ms-List-cell),
|
||||
.bingGrildList :global(.ms-List-page) {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,579 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Good looks
|
||||
import styles from './WebSearchTab.module.scss';
|
||||
|
||||
// Localization
|
||||
import * as strings from 'PropertyPaneFilePickerStrings';
|
||||
|
||||
// Types
|
||||
import { IWebSearchTabProps, IWebSearchTabState, ISearchSuggestion, ISearchResult, ImageSize, ImageAspect, ImageLicense } from './WebSearchTab.types';
|
||||
|
||||
// Offce Fabric stuff
|
||||
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/components/Button';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
|
||||
import { Check } from 'office-ui-fabric-react/lib/Check';
|
||||
import { Dropdown, IDropdownProps, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
|
||||
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import { Link } from 'office-ui-fabric-react/lib/Link';
|
||||
import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone';
|
||||
import { List } from 'office-ui-fabric-react/lib/List';
|
||||
import { IRectangle } from 'office-ui-fabric-react/lib/Utilities';
|
||||
import { Selection, SelectionMode, SelectionZone } from 'office-ui-fabric-react/lib/Selection';
|
||||
import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar';
|
||||
|
||||
// Used for HTTP requests
|
||||
import { SPHttpClient, IHttpClientOptions, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
|
||||
// CSS utility to combine classes dynamically
|
||||
import { css } from '@uifabric/utilities/lib/css';
|
||||
|
||||
/**
|
||||
* Rows per page
|
||||
*/
|
||||
const ROWS_PER_PAGE = 3;
|
||||
|
||||
/**
|
||||
* Maximum row height
|
||||
*/
|
||||
const MAX_ROW_HEIGHT = 250;
|
||||
|
||||
/**
|
||||
* Maximum file size when searching
|
||||
*/
|
||||
const MAXFILESIZE = 52428800;
|
||||
|
||||
/**
|
||||
* Maximum number of search results
|
||||
*/
|
||||
const MAXRESULTS = 100;
|
||||
|
||||
/**
|
||||
* This is the default search suggestions as of Jan 2019.
|
||||
* I have no idea where Bing gets them.
|
||||
* But you can provide your own when calling this component
|
||||
*/
|
||||
const DEFAULT_SUGGESTIONS: ISearchSuggestion[] = [
|
||||
{
|
||||
topic: 'Backgrounds',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/background_b4f5f0fd0af42d6dc969f795cb65a13c.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Classrooms',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/classroom_a0b3addf2246028cb7486ddfb0783c6c.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Conferences',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/conference_b450359b0962cf464f691b68d7c6ecd1.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Meetings',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/meeting_694397debfa52bc06a952310af01d59d.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Patterns',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/pattern_6e7c8fd91c9b5fa47519aa3fd4a95a82.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Teamwork',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/teamwork_5841da2ae9b9424173f601d86e3a252c.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Technology',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/technology_9a8a4e09c090c65f4c0b3ea06bd48b83.jpg'
|
||||
},
|
||||
{
|
||||
topic: 'Scenery',
|
||||
backgroundUrl: 'https://spoprod-a.akamaihd.net/files/sp-client-prod_2019-01-11.008/scenery_abe5bfb8f3913bd52be279a793472ead.jpg'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* The tenant storage key to use when storing the Bing API key.
|
||||
*/
|
||||
const BINGAPI_TENANT_STORAGEKEY: string = 'BingApi';
|
||||
|
||||
/**
|
||||
* Renders search suggestions and performs seach queries
|
||||
*/
|
||||
export default class WebSearchTab extends React.Component<IWebSearchTabProps, IWebSearchTabState> {
|
||||
private _columnCount: number;
|
||||
private _columnWidth: number;
|
||||
private _rowHeight: number;
|
||||
private _selection: Selection;
|
||||
private _listElem: List = undefined;
|
||||
|
||||
constructor(props: IWebSearchTabProps) {
|
||||
super(props);
|
||||
|
||||
this._selection = new Selection(
|
||||
{
|
||||
selectionMode: SelectionMode.single,
|
||||
onSelectionChanged: () => {
|
||||
// Get the selected item
|
||||
const selectedItems = this._selection.getSelection();
|
||||
if (selectedItems && selectedItems.length > 0) {
|
||||
//Get the selected key
|
||||
const selectedKey: ISearchResult = selectedItems[0] as ISearchResult;
|
||||
|
||||
//Brute force approach to making sure all URLs are loading over HTTPS
|
||||
// even if it breaks the page.
|
||||
const selectedUrl: string = selectedKey.contentUrl.replace('http://', 'https://');
|
||||
|
||||
// Save the selected file
|
||||
this.setState({
|
||||
fileUrl: selectedUrl
|
||||
});
|
||||
} else {
|
||||
// Remove any selected file
|
||||
this.setState({
|
||||
fileUrl: undefined
|
||||
});
|
||||
}
|
||||
if (this._listElem) {
|
||||
// Force the list to update to show the selection check
|
||||
this._listElem.forceUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
hasKey: undefined,
|
||||
results: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API key
|
||||
*/
|
||||
public componentDidMount(): void {
|
||||
// Find out if we should show anything
|
||||
this._getAPIKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the tab
|
||||
*/
|
||||
public render(): React.ReactElement<IWebSearchTabProps> {
|
||||
const { hasKey, query, results } = this.state;
|
||||
return (
|
||||
<div className={styles.tabContainer}>
|
||||
<div className={styles.tabHeaderContainer}>
|
||||
<h2 className={styles.tabHeader}>{strings.WebSearchLinkLabel}</h2>
|
||||
{hasKey && this._renderSearchBox()}
|
||||
</div>
|
||||
<div className={styles.tab}>
|
||||
{hasKey === false && strings.SorryWebSearch} {/* If we verified we don't have a key, give a little Sorry message */}
|
||||
{hasKey && !query && this._renderSearchSuggestions()} {/* No search yet, show suggestions */}
|
||||
{query && results && this._renderSearchResults()} {/* Got results, show them */}
|
||||
</div>
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
{this.state.results && this.state.license === 'Any' && <MessageBar>
|
||||
{strings.CreativeCommonsMessage}
|
||||
</MessageBar>}
|
||||
<Label className={styles.copyrightLabel}>
|
||||
{strings.CopyrightWarning}
|
||||
<Link target='_blank' href={strings.CopyrightUrl}>{strings.LearnMoreLink}</Link>
|
||||
</Label>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<PrimaryButton
|
||||
disabled={this.state.fileUrl === undefined}
|
||||
className={styles.actionButton}
|
||||
onClick={() => this._handleSave()}
|
||||
>{strings.OpenButtonLabel}</PrimaryButton>
|
||||
<DefaultButton onClick={() => this._handleClose()} className={styles.actionButton}>{strings.CancelButtonLabel}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the returned search results
|
||||
*/
|
||||
private _renderSearchResults = (): JSX.Element => {
|
||||
const { results } = this.state;
|
||||
|
||||
// If there are no results, tell 'em.
|
||||
if (results === undefined || results.length < 1) {
|
||||
return <Label className={styles.noResultLabel}>{strings.NoResultsBadEnglish}</Label>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusZone>
|
||||
<SelectionZone selection={this._selection}
|
||||
onItemInvoked={(item: ISearchResult) => this._selection.setKeySelected(item.key, true, true)}
|
||||
>
|
||||
<List
|
||||
ref={this._linkElement}
|
||||
className={styles.bingGrildList}
|
||||
items={this.state.results}
|
||||
getItemCountForPage={this._getItemCountForPage}
|
||||
getPageHeight={this._getPageHeight}
|
||||
renderedWindowsAhead={4}
|
||||
onRenderCell={this._onRenderSearchResultsCell}
|
||||
/>
|
||||
</SelectionZone>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an individual search result item
|
||||
*/
|
||||
private _onRenderSearchResultsCell = (item: ISearchResult, index: number | undefined): JSX.Element => {
|
||||
const { query } = this.state;
|
||||
|
||||
let isSelected: boolean = false;
|
||||
|
||||
if (this._selection && index !== undefined) {
|
||||
isSelected = this._selection.isIndexSelected(index);
|
||||
}
|
||||
|
||||
// The logic for calculating the thumbnail dimensions is not quite the same as the out-of-the-box file picker,
|
||||
// but it'll have to do.
|
||||
|
||||
// Find the aspect ratio of the picture
|
||||
const ratio: number = item.width / item.height;
|
||||
|
||||
// Fit the height to the desired row height
|
||||
let thumbnailHeight: number = Math.min(this._rowHeight, item.height);
|
||||
|
||||
// Resize the picture with the same aspect ratio
|
||||
let thumbnailWidth: number = thumbnailHeight * ratio;
|
||||
|
||||
const searchResultAltText: string = strings.SearchResultAlt.replace('{0}', query);
|
||||
return (
|
||||
<div
|
||||
className={styles.bingGridListCell}
|
||||
style={{
|
||||
width: 100 / this._columnCount + '%'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-label={searchResultAltText}
|
||||
className={css(styles.bingTile, isSelected ? styles.isSelected : undefined)}
|
||||
data-is-focusable={true}
|
||||
data-selection-index={index}
|
||||
style={{
|
||||
width: `${thumbnailWidth}px`,
|
||||
height: `${thumbnailHeight}px`
|
||||
}}>
|
||||
<div className={styles.bingTileContent} data-selection-invoke={true}>
|
||||
<Image src={item.thumbnailUrl} className={styles.bingTileThumbnail} alt={searchResultAltText} width={thumbnailWidth} height={thumbnailHeight} />
|
||||
<div className={styles.bingTileFrame}></div>
|
||||
<div className={styles.bingTileCheckCircle}
|
||||
role='checkbox'
|
||||
aria-checked={isSelected}
|
||||
data-item-index={index} data-selection-toggle={true} data-automationid='CheckCircle'>
|
||||
<Check checked={isSelected} />
|
||||
</div>
|
||||
<div className={styles.bingTileNamePlate}>
|
||||
<Link
|
||||
href={item.contentUrl}
|
||||
target='_blank'
|
||||
aria-label={strings.SearchResultAriaLabel}
|
||||
>{item.displayUrl}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders suggestions when there aren't any queries
|
||||
*/
|
||||
private _renderSearchSuggestions = (): JSX.Element => {
|
||||
const suggestions: ISearchSuggestion[] = this.props.suggestions !== undefined ? this.props.suggestions : DEFAULT_SUGGESTIONS;
|
||||
|
||||
return (
|
||||
<FocusZone>
|
||||
<List
|
||||
className={styles.filePickerFolderCardGrid}
|
||||
items={suggestions}
|
||||
getItemCountForPage={this._getItemCountForPage}
|
||||
getPageHeight={this._getPageHeight}
|
||||
renderedWindowsAhead={4}
|
||||
onRenderCell={this._onRenderSuggestionCell}
|
||||
/>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets search results from Bing
|
||||
*/
|
||||
private _getSearchResults = () => {
|
||||
const aspect: string = this.state.aspect ? this.state.aspect : 'All';
|
||||
const size: string = this.state.size ? this.state.size : 'All';
|
||||
const license: string = this.state.license ? this.state.license : 'Any';
|
||||
const { query } = this.state;
|
||||
|
||||
if (query === undefined) {
|
||||
// No query
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a loading indicator
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
// Use this client option to prevent CORS issues.
|
||||
const httpClientOptions: IHttpClientOptions = {
|
||||
headers: new Headers(),
|
||||
method: 'GET',
|
||||
mode: 'cors'
|
||||
};
|
||||
|
||||
// Submit the request
|
||||
const apiUrl: string = `https://www.bingapis.com/api/v7/images/search?appid=${this.state.apiKey}&traffictype=Internal_monitor&q=${encodeURIComponent(query)}&count=${MAXRESULTS}&aspect=${aspect}&maxFileSize=${MAXFILESIZE}&mkt=en-US&size=${size}&license=${license}`;
|
||||
this.props.context.httpClient.get(apiUrl,
|
||||
SPHttpClient.configurations.v1, httpClientOptions)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
response.json().then((responseJSON: any) => {
|
||||
// Cast to Bing search results (for type safety)
|
||||
const bingResults: IBingSearchResult[] = responseJSON.value;
|
||||
|
||||
//Convert results to search results
|
||||
const searchResults: ISearchResult[] = bingResults.map((bingResult: IBingSearchResult) => {
|
||||
// Get dimensions
|
||||
const width: number = bingResult!.thumbnail!.width ? bingResult!.thumbnail!.width : bingResult!.width;
|
||||
const height: number = bingResult!.thumbnail!.height ? bingResult!.thumbnail!.height : bingResult!.height;
|
||||
|
||||
// Create a search result
|
||||
const searchResult: ISearchResult = {
|
||||
thumbnailUrl: bingResult.thumbnailUrl,
|
||||
contentUrl: bingResult.contentUrl,
|
||||
displayUrl: this.getDisplayUrl(bingResult.hostPageDisplayUrl),
|
||||
key: bingResult.imageId,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
return searchResult;
|
||||
});
|
||||
|
||||
// Set the items so that the selection zone can keep track of them
|
||||
this._selection.setItems(searchResults, true);
|
||||
|
||||
// Save results and stop loading indicator
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
results: searchResults
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many items there should be in the page
|
||||
*/
|
||||
private _getItemCountForPage = (itemIndex: number, surfaceRect: IRectangle): number => {
|
||||
if (itemIndex === 0) {
|
||||
this._columnCount = Math.ceil(surfaceRect.width / MAX_ROW_HEIGHT);
|
||||
this._columnWidth = Math.floor(surfaceRect.width / this._columnCount);
|
||||
this._rowHeight = this._columnWidth;
|
||||
}
|
||||
|
||||
return this._columnCount * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the height of a list "page"
|
||||
*/
|
||||
private _getPageHeight = (): number => {
|
||||
return this._rowHeight * ROWS_PER_PAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a cell for search suggestions
|
||||
*/
|
||||
private _onRenderSuggestionCell = (item: ISearchSuggestion, index: number | undefined): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={styles.filePickerFolderCardTile}
|
||||
data-is-focusable={true}
|
||||
style={{
|
||||
width: 100 / this._columnCount + '%'
|
||||
}}
|
||||
>
|
||||
<div className={styles.filePickerFolderCardSizer}>
|
||||
<div className={styles.filePickerFolderCardPadder}>
|
||||
<Image src={item.backgroundUrl} className={styles.filePickerFolderCardImage} imageFit={ImageFit.cover} />
|
||||
<DefaultButton className={styles.filePickerFolderCardLabel} onClick={(_event) => this._handleSearch(item.topic)}>{item.topic}</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the search box
|
||||
*/
|
||||
private _renderSearchBox = (): JSX.Element => {
|
||||
const { query } = this.state;
|
||||
const hasQuery: boolean = query !== undefined;
|
||||
const license: string = this.state.license ? this.state.license : 'All';
|
||||
return <div className={styles.searchBoxContainer}>
|
||||
<div className={styles.searchBoxMedium}>
|
||||
<div className={styles.searchBox}>
|
||||
<SearchBox
|
||||
placeholder={strings.SearchBoxPlaceholder}
|
||||
value={query}
|
||||
onSearch={newQuery => this._handleSearch(newQuery)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Label>{strings.PoweredByBing}</Label>
|
||||
{hasQuery && <div className={styles.dropdownContainer}>
|
||||
<Dropdown
|
||||
className={styles.filterDropdown}
|
||||
placeholder={strings.ImageSizePlaceholderText}
|
||||
onRenderPlaceHolder={(props: IDropdownProps) => this._renderFilterPlaceholder(props)}
|
||||
selectedKey={this.state.size}
|
||||
options={[
|
||||
{ key: 'All', text: strings.SizeOptionAll },
|
||||
{ key: 'Small', text: strings.SizeOptionSmall },
|
||||
{ key: 'Medium', text: strings.SizeOptionMedium },
|
||||
{ key: 'Large', text: strings.SizeOptionLarge },
|
||||
{ key: 'Wallpaper', text: strings.SizeOptionExtraLarge }
|
||||
]}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => this._handleChangeSize(option)}
|
||||
/>
|
||||
<Dropdown
|
||||
className={styles.filterDropdown}
|
||||
placeholder={strings.ImageLayoutPlaceholderText}
|
||||
onRenderPlaceHolder={(props: IDropdownProps) => this._renderFilterPlaceholder(props)}
|
||||
selectedKey={this.state.aspect}
|
||||
options={[
|
||||
{ key: 'All', text: strings.LayoutOptionAll },
|
||||
{ key: 'Square', text: strings.LayoutOptionSquare },
|
||||
{ key: 'Wide', text: strings.LayoutOptionWide },
|
||||
{ key: 'Tall', text: strings.LayoutOptionTall },
|
||||
]}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => this._handleChangeLayout(option)}
|
||||
/>
|
||||
<Dropdown
|
||||
className={styles.filterDropdown}
|
||||
placeholder={strings.LicensePlaceholderText}
|
||||
onRenderPlaceHolder={(props: IDropdownProps) => this._renderFilterPlaceholder(props)}
|
||||
selectedKey={license}
|
||||
options={[
|
||||
{ key: 'All', text: strings.LicenseOptionAll },
|
||||
{ key: 'Any', text: strings.LicenseOptionAny }
|
||||
]}
|
||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => this._handleChangeLicense(option)}
|
||||
/>
|
||||
</div>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when a user changes the size drop down.
|
||||
* Resubmits search query
|
||||
*/
|
||||
private _handleChangeSize = (option: IDropdownOption) => {
|
||||
this.setState({
|
||||
size: option.key as ImageSize
|
||||
}, () => this._getSearchResults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when user selects a new layout from the drop down.
|
||||
* Resubmits search query.
|
||||
*/
|
||||
private _handleChangeLayout = (option: IDropdownOption) => {
|
||||
this.setState({
|
||||
aspect: option.key as ImageAspect
|
||||
}, () => this._getSearchResults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when a user changes the license from the drop down
|
||||
* Resubits search query
|
||||
*/
|
||||
private _handleChangeLicense = (option: IDropdownOption) => {
|
||||
this.setState({
|
||||
license: option.key as ImageLicense
|
||||
}, () => this._getSearchResults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the drop down placeholders
|
||||
*/
|
||||
private _renderFilterPlaceholder = (props: IDropdownProps): JSX.Element => {
|
||||
return <span>{props.placeholder}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when user triggers search query
|
||||
*/
|
||||
private _handleSearch = (newQuery?: string) => {
|
||||
this.setState({
|
||||
query: newQuery
|
||||
}, () => this._getSearchResults());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles when user closes search pane
|
||||
*/
|
||||
private _handleClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handes when user saves selection
|
||||
* Calls property pane file picker's save function
|
||||
*/
|
||||
private _handleSave = () => {
|
||||
this.props.onSave(encodeURI(this.state.fileUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes protocol and retrieves only the domain, just like Bing search results does
|
||||
* in the SharePoint file picker
|
||||
* @param url The display url as provided by Bing
|
||||
*/
|
||||
private getDisplayUrl(url: string): string {
|
||||
// remove any protocols
|
||||
if (url.indexOf('://') > -1) {
|
||||
const urlParts: string[] = url.split('://');
|
||||
url = urlParts.pop();
|
||||
}
|
||||
|
||||
// Split the URL on the first slash
|
||||
const splitUrl = url.split('/');
|
||||
return splitUrl[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the API key from tenant storage
|
||||
*/
|
||||
private _getAPIKey() {
|
||||
const { absoluteUrl } = this.props.context.pageContext.web;
|
||||
const apiUrl: string = `${absoluteUrl}/_api/web/GetStorageEntity('${BINGAPI_TENANT_STORAGEKEY}')`;
|
||||
this.props.context.spHttpClient.get(apiUrl, SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
response.json().then((responseJSON: any) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
apiKey: responseJSON.Value,
|
||||
hasKey: responseJSON.Value !== undefined
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
hasKey: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reference to the list
|
||||
*/
|
||||
private _linkElement = (e: any) => {
|
||||
this._listElem = e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { IFilePickerTab } from "../IFilePickerTab.types";
|
||||
|
||||
export interface IWebSearchTabProps extends IFilePickerTab {
|
||||
suggestions?: ISearchSuggestion[];
|
||||
}
|
||||
|
||||
export interface IWebSearchTabState {
|
||||
isLoading: boolean;
|
||||
apiKey?: string;
|
||||
hasKey?: boolean;
|
||||
query?: string;
|
||||
size?: ImageSize;
|
||||
aspect?: ImageAspect;
|
||||
license?: ImageLicense;
|
||||
results: ISearchResult[];
|
||||
fileUrl?: string;
|
||||
}
|
||||
|
||||
export interface ISearchSuggestion {
|
||||
topic: string;
|
||||
backgroundUrl: string;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
thumbnailUrl: string;
|
||||
contentUrl: string;
|
||||
displayUrl: string;
|
||||
key: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type ImageSize = 'All' | 'Small' | 'Medium' | 'Large' | 'Wallpaper';
|
||||
|
||||
export type ImageAspect = 'All' | 'Square' | 'Wide' | 'Tall';
|
||||
|
||||
export type ImageLicense = 'All' | 'Any';
|
|
@ -0,0 +1,2 @@
|
|||
export * from './WebSearchTab';
|
||||
export * from './WebSearchTab.types';
|
|
@ -0,0 +1,4 @@
|
|||
export * from './IPropertyPaneFilePicker';
|
||||
export * from './IPropertyPaneFilePickerHost';
|
||||
export * from './PropertyPaneFilePickerHost';
|
||||
export * from './PropertyPaneFilePicker';
|
|
@ -0,0 +1,115 @@
|
|||
define([], function () {
|
||||
return {
|
||||
"AddFileButtonLabel": "Add file",
|
||||
"AddImageButtonLabel": "Add image",
|
||||
"AriaCellValue": "{0} column, {1}",
|
||||
"CancelButtonLabel": "Cancel",
|
||||
"CantValidateValidationMessage": "We couldn't verify this link. Please check the link and try again.",
|
||||
"ChangeFileLinkLabel": "Change file",
|
||||
"ChangeImageLinkLabel": "Change image",
|
||||
"ChooseFileLinkLabel": "Choose file",
|
||||
"ChooseImageLinkLabel": "Choose image",
|
||||
"CopyrightUrl": "https://www.microsoft.com/en-US/legal/copyright/default.aspx",
|
||||
"CopyrightWarning": "You are responsible for respecting others' rights, including copyright.",
|
||||
"CreativeCommonsMessage": "These results are tagged with Creative Commons licenses. Review the licenses to ensure you comply.",
|
||||
"DateFormat": "MM/DD/YYYY hh:mm A",
|
||||
"DocumentLabelTemplate": "{0}, Document, Modified {1}, edited by {2}, Private",
|
||||
"DocumentLibraries": "Document libraries",
|
||||
"EditedByNamePlate": "edited by ",
|
||||
"EmptyFileSize": "0 bytes",
|
||||
"FilePickerHeader": "File Picker",
|
||||
"FileSizeField": "File Size",
|
||||
"FolderAltText": "Folder",
|
||||
"FolderBackPlate": "https://static2.sharepointonline.com/files/fabric/office-ui-fabric-react-assets/foldericons/folder-large_backplate.svg",
|
||||
"FolderFrontPlate": "https://static2.sharepointonline.com/files/fabric/office-ui-fabric-react-assets/foldericons/folder-large_frontplate_nopreview.svg",
|
||||
"FolderIconUrl": "https://spoprod-a.akamaihd.net/files/odsp-next-prod_2019-01-11_20190116.001/odsp-media/images/itemtypes/20/folder.svg",
|
||||
"FolderLabelTemplate": "{0}, Folder, Modified {1}, edited by {2}, {3} items, Private",
|
||||
"FromLinkLinkLabel": "From a link",
|
||||
"ImageAltText": ".{0} Image",
|
||||
"ImageAriaLabelTemplate": ".{0} Image",
|
||||
"ImageLayoutPlaceholderText": "Layout",
|
||||
"ImageSizePlaceholderText": "Image size",
|
||||
"ItemChildCountField": "Item Child Count",
|
||||
"LayoutOptionAll": "All",
|
||||
"LayoutOptionSquare": "Square",
|
||||
"LayoutOptionTall": "Tall",
|
||||
"LayoutOptionWide": "Wide",
|
||||
"LearnMoreLink": "Learn more.",
|
||||
"LicenseOptionAll": "All",
|
||||
"LicenseOptionAny": "Creative Commons only",
|
||||
"LicensePlaceholderText": "License",
|
||||
"LinkFileInstructions": "Paste a link to a file in OneDrive for Business or SharePoint Online",
|
||||
"LinkHeader": "From a link",
|
||||
"LinkImageInstructions": "Paste a link to an image in OneDrive for Business or SharePoint Online",
|
||||
"ListLayoutAriaLabel": "View options. {0} {1} .",
|
||||
"ListLayoutCompact": "Compact view",
|
||||
"ListLayoutCompactDescription": "View items and details in a compact list",
|
||||
"ListLayoutList": "List view",
|
||||
"ListLayoutListDescrition": "View items and details in a list",
|
||||
"ListLayoutTile": "Tile view",
|
||||
"ListLayoutTileDescription": "View items with tile previews",
|
||||
"ListOptionsAlt": "View options. {0} selected .",
|
||||
"ListOptionsTitle": "Open the view options menu",
|
||||
"Loading": "Loading...",
|
||||
"ModifiedByField": "Modified By",
|
||||
"ModifiedField": "Date Modified",
|
||||
"NameField": "Name",
|
||||
"No": "No",
|
||||
"NoExternalLinksValidationMessage": "We only support linking to files in your own organization.",
|
||||
"NoImageValidationMessage": "This isn't a link to a file type we support. You can only link to an image.",
|
||||
"NoRecentFiles": "No recent files",
|
||||
"NoRecentFilesDescription": "Try selecting a file from your site, or upload one from your device.",
|
||||
"NoResultsBadEnglish": "***There is no result found. Try to change the filter options",
|
||||
"ODModifiedField": "Modified",
|
||||
"ODPhotoIconUrl": "https://spoprod-a.akamaihd.net/files/odsp-next-prod_2019-01-18_20190124.001/odsp-media/images/itemtypes/16_2x/photo.png",
|
||||
"ODRowArialLabelTemplate": "{0}, {1}, Modified {2}, edited by {3}, {4}, {5}",
|
||||
"OneDriveConfirmDialogBody": "This item is from your OneDrive site. Files and folders in OneDrive are private unless you share them. Have you shared this file with your site members so they can access it?",
|
||||
"OneDriveConfirmDialogTitle": "Just checking...",
|
||||
"OneDriveEmptyFolderAlt": "Empty folder",
|
||||
"OneDriveEmptyFolderDescription": "To add files, go to your OneDrive. You can also add files to this folder using the OneDrive app for your computer.",
|
||||
"OneDriveEmptyFolderIconUrl": "https://spoprod-a.akamaihd.net/files/odsp-next-prod_2019-01-18_20190124.001/odsp-media/images/emptyfolder/empty_folder.svg",
|
||||
"OneDriveEmptyFolderTitle": "This folder is empty",
|
||||
"OneDriveRootFolderName": "Files",
|
||||
"OpenButtonLabel": "Open",
|
||||
"PhotoIconUrl": "https://spoprod-a.akamaihd.net/files/odsp-next-prod_2019-01-11_20190116.001/odsp-media/images/itemtypes/20_2x/photo.png",
|
||||
"PoweredByBing": "Powered by Bing",
|
||||
"RecentDocumentsHeader": "Recent documents",
|
||||
"RecentImagesHeader": "Recent images",
|
||||
"RecentLinkLabel": "Recent",
|
||||
"SearchBoxPlaceholder": "Web search",
|
||||
"SearchResultAlt": "Image result for {0}.",
|
||||
"SearchResultAriaLabel": "Press enter to open the image source in a new tab.",
|
||||
"Selected": "selected",
|
||||
"SharingField": "Sharing",
|
||||
"SharingPrivate": "Private",
|
||||
"SharingShared": "Shared",
|
||||
"SiteLinkLabel": "Site",
|
||||
"SizeOptionAll": "All",
|
||||
"SizeOptionExtraLarge": "Extra Large",
|
||||
"SizeOptionLarge": "Large",
|
||||
"SizeOptionMedium": "Medium",
|
||||
"SizeOptionSmall": "Small",
|
||||
"SizeUnit": [
|
||||
"bytes",
|
||||
"KB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB",
|
||||
"PB",
|
||||
"EB",
|
||||
"ZB",
|
||||
"YB"
|
||||
],
|
||||
"SorryWebSearch": "Sorry, this function isn't implemented in this sample, because it would require a Bing API key.",
|
||||
"SortedAscending": "Sorted A to Z",
|
||||
"SortedDescending": "Sorted Z to A",
|
||||
"TypeAriaLabel": "Column operations for File type, Press to sort on File type",
|
||||
"UploadFileHeader": "Upload file",
|
||||
"UploadImageHeader": "Upload image",
|
||||
"UploadLinkLabel": "Upload",
|
||||
"WebSearchLinkLabel": "Web search",
|
||||
"Yes": "Yes"
|
||||
}
|
||||
});
|
||||
|
||||
|
107
samples/react-comparer/src/controls/PropertyPaneFilePicker/loc/propertypaneimagepickerstrings.d.ts
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
declare interface IPropertyPaneFilePickerStrings {
|
||||
AddFileButtonLabel: string;
|
||||
AddImageButtonLabel: string;
|
||||
AriaCellValue: string;
|
||||
CancelButtonLabel: string;
|
||||
CantValidateValidationMessage: string;
|
||||
ChangeFileLinkLabel: string;
|
||||
ChangeImageLinkLabel: string;
|
||||
ChooseFileLinkLabel: string;
|
||||
ChooseImageLinkLabel: string;
|
||||
CopyrightUrl: string;
|
||||
CopyrightWarning: string;
|
||||
CreativeCommonsMessage: string;
|
||||
DateFormat: string;
|
||||
DocumentLabelTemplate: string;
|
||||
DocumentLibraries: string;
|
||||
EditedByNamePlate: string;
|
||||
EmptyFileSize: string;
|
||||
FilePickerHeader: string;
|
||||
FileSizeField: string;
|
||||
FolderAltText: string;
|
||||
FolderBackPlate: string;
|
||||
FolderFrontPlate: string;
|
||||
FolderIconUrl: string;
|
||||
FolderLabelTemplate: string;
|
||||
FromLinkLinkLabel: string;
|
||||
ImageAltText: string;
|
||||
ImageAriaLabelTemplate: string;
|
||||
ImageLayoutPlaceholderText: string;
|
||||
ImageSizePlaceholderText: string;
|
||||
ImageSizePlaceholderText: string;
|
||||
ItemChildCountField: string;
|
||||
LayoutOptionAll: string;
|
||||
LayoutOptionSquare: string;
|
||||
LayoutOptionTall: string;
|
||||
LayoutOptionWide: string;
|
||||
LearnMoreLink: string;
|
||||
LicenseOptionAll: string;
|
||||
LicenseOptionAny: string;
|
||||
LicensePlaceholderText: string;
|
||||
LinkFileInstructions: string;
|
||||
LinkHeader: string;
|
||||
LinkImageInstructions: string;
|
||||
ListLayoutAriaLabel: string;
|
||||
ListLayoutCompact: string;
|
||||
ListLayoutCompactDescription: string;
|
||||
ListLayoutList: string;
|
||||
ListLayoutListDescrition: string;
|
||||
ListLayoutTile: string;
|
||||
ListLayoutTileDescription: string;
|
||||
ListOptionsAlt: string;
|
||||
ListOptionsTitle: string;
|
||||
Loading: string;
|
||||
ModifiedByField: string;
|
||||
ModifiedField: string;
|
||||
NameField: string;
|
||||
No: string;
|
||||
NoExternalLinksValidationMessage: string;
|
||||
NoImageValidationMessage: string;
|
||||
NoRecentFiles: string;
|
||||
NoRecentFilesDescription: string;
|
||||
NoResultsBadEnglish: string;
|
||||
ODModifiedField: string;
|
||||
ODPhotoIconUrl: string;
|
||||
ODRowArialLabelTemplate: string;
|
||||
OneDriveConfirmDialogBody: string;
|
||||
OneDriveConfirmDialogTitle: string;
|
||||
OneDriveEmptyFolderAlt: string;
|
||||
OneDriveEmptyFolderDescription: string;
|
||||
OneDriveEmptyFolderIconUrl: string;
|
||||
OneDriveEmptyFolderTitle: string;
|
||||
OneDriveRootFolderName: string;
|
||||
OpenButtonLabel: string;
|
||||
PhotoIconUrl: string;
|
||||
PoweredByBing: string;
|
||||
RecentDocumentsHeader: string;
|
||||
RecentImagesHeader: string;
|
||||
RecentLinkLabel: string;
|
||||
SearchBoxPlaceholder: string;
|
||||
SearchResultAlt: string;
|
||||
SearchResultAriaLabel: string;
|
||||
Selected: string;
|
||||
SharingField: string;
|
||||
SharingPrivate: string;
|
||||
SharingShared: string;
|
||||
SiteLinkLabel: string;
|
||||
SizeOptionAll: string;
|
||||
SizeOptionExtraLarge: string;
|
||||
SizeOptionLarge: string;
|
||||
SizeOptionMedium: string;
|
||||
SizeOptionSmall: string;
|
||||
SizeUnit: string[];
|
||||
SorryWebSearch: string;
|
||||
SortedAscending: string;
|
||||
SortedDescending: string;
|
||||
TypeAriaLabel: string;
|
||||
UploadFileHeader: string;
|
||||
UploadImageHeader: string;
|
||||
UploadLinkLabel: string;
|
||||
WebSearchLinkLabel: string;
|
||||
Yes: string;
|
||||
}
|
||||
|
||||
declare module 'PropertyPaneFilePickerStrings' {
|
||||
const strings: IPropertyPaneFilePickerStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
export interface IDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export interface IGetListDataAsStreamRequest {
|
||||
parameters:{
|
||||
RenderOptions :number;
|
||||
ViewXml:string;
|
||||
AllowMultipleValueFilterForTaxonomyFields:boolean;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
// Generated by https://quicktype.io
|
||||
|
||||
export interface IGetListDataAsStreamResult {
|
||||
wpq: string;
|
||||
Templates: ITemplates;
|
||||
ListData: IListData;
|
||||
ListSchema: IListSchema;
|
||||
ViewMetadata: IViewMetadata;
|
||||
BaseViewID: string;
|
||||
ListTemplateType: string;
|
||||
listBaseType: number;
|
||||
noGroupCollapse: boolean;
|
||||
InlineEdit: boolean;
|
||||
NavigateForFormsPages: boolean;
|
||||
BasePermissions: IBasePermissions;
|
||||
CurrentUserIsSiteAdmin: boolean;
|
||||
IsAppWeb: boolean;
|
||||
AllowGridMode: boolean;
|
||||
inGridMode: boolean;
|
||||
listTemplate: string;
|
||||
listName: string;
|
||||
rootFolder: string;
|
||||
view: string;
|
||||
viewTitle: string;
|
||||
listUrlDir: string;
|
||||
HttpPath: string;
|
||||
HttpRoot: string;
|
||||
NoScriptEnabled: boolean;
|
||||
imagesPath: string;
|
||||
PortalUrl: null;
|
||||
SendToLocationName: string;
|
||||
SendToLocationUrl: string;
|
||||
RecycleBinEnabled: number;
|
||||
OfficialFileName: string;
|
||||
OfficialFileNames: string;
|
||||
WriteSecurity: string;
|
||||
SiteTitle: string;
|
||||
ListTitle: string;
|
||||
displayFormUrl: string;
|
||||
newFormUrl: string;
|
||||
editFormUrl: string;
|
||||
ctxId: number;
|
||||
isXslView: boolean;
|
||||
IsClientRendering: boolean;
|
||||
CurrentUserId: number;
|
||||
isModerated: boolean;
|
||||
EnableRequestSignOff: boolean;
|
||||
isForceCheckout: boolean;
|
||||
EnableMinorVersions: boolean;
|
||||
verEnabled: boolean;
|
||||
recursiveView: boolean;
|
||||
WorkflowsAssociated: boolean;
|
||||
ContentTypesEnabled: boolean;
|
||||
DocumentLibraryCalloutOfficeWebAppPreviewersDisabled: boolean;
|
||||
RegionalSettingsTimeZoneBias: number;
|
||||
NewWOPIDocumentEnabled: boolean;
|
||||
NewWOPIDocumentUrl: string;
|
||||
NewWOPIDocumentTypes: number;
|
||||
canUserCreateMicrosoftForm: boolean;
|
||||
AllowCreateFolder: boolean;
|
||||
CanShareLinkForNewDocument: boolean;
|
||||
VisioDrawingCreationEnabled: boolean;
|
||||
SiteTemplateId: number;
|
||||
TenantTagPolicyEnabled: boolean;
|
||||
WebExcludeFromOfflineClient: boolean;
|
||||
ExcludeFromOfflineClient: boolean;
|
||||
ParentInfo: IParentInfo;
|
||||
}
|
||||
|
||||
export interface IBasePermissions {
|
||||
ManageLists: boolean;
|
||||
ManagePersonalViews: boolean;
|
||||
OpenItems: boolean;
|
||||
UseClientIntegration: boolean;
|
||||
}
|
||||
|
||||
export interface IListData {
|
||||
Row: IRow[];
|
||||
FirstRow: number;
|
||||
FolderPermissions: string;
|
||||
LastRow: number;
|
||||
RowLimit: number;
|
||||
FilterLink: string;
|
||||
ForceNoHierarchy: string;
|
||||
HierarchyHasIndention: string;
|
||||
FolderId: string;
|
||||
CurrentFolderProgId: string;
|
||||
}
|
||||
|
||||
export interface IRow {
|
||||
ID: string;
|
||||
PermMask: string;
|
||||
FSObjType: string;
|
||||
HTML_x0020_File_x0020_Type: string;
|
||||
UniqueId: string;
|
||||
ProgId: string;
|
||||
NoExecute: string;
|
||||
File_x0020_Type: string;
|
||||
"File_x0020_Type.mapapp": string;
|
||||
"HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon": string;
|
||||
"HTML_x0020_File_x0020_Type.File_x0020_Type.mapico": string;
|
||||
"serverurl.progid": string;
|
||||
ServerRedirectedEmbedUrl: string;
|
||||
"File_x0020_Type.progid": string;
|
||||
"File_x0020_Type.url": string;
|
||||
FileRef: string;
|
||||
FileLeafRef: string;
|
||||
CheckoutUser: string;
|
||||
CheckedOutUserId: string;
|
||||
IsCheckedoutToLocal: string;
|
||||
_ComplianceFlags: string;
|
||||
_ShortcutUrl: string;
|
||||
"_ShortcutUrl.desc": string;
|
||||
_ShortcutSiteId: string;
|
||||
_ShortcutWebId: string;
|
||||
_ShortcutUniqueId: string;
|
||||
"Created_x0020_Date.ifnew": string;
|
||||
ContentTypeId: string;
|
||||
Modified: string;
|
||||
"Modified.FriendlyDisplay": string;
|
||||
Editor: IEditor[];
|
||||
File_x0020_Size: string;
|
||||
PrincipalCount: string;
|
||||
MediaServiceFastMetadata: string;
|
||||
MediaServiceOCR: string;
|
||||
ItemChildCount: string;
|
||||
FolderChildCount: string;
|
||||
SMTotalFileCount: string;
|
||||
SMTotalSize: string;
|
||||
SortBehavior: string;
|
||||
FileSizeDisplay: string;
|
||||
_ComplianceTag: string;
|
||||
ContentVersion: string;
|
||||
DocConcurrencyNumber: string;
|
||||
_VirusStatus: string;
|
||||
".spItemUrl": string;
|
||||
".fileType": string;
|
||||
".hasThumbnail": string;
|
||||
".hasVideoManifest": string;
|
||||
".hasPdf": string;
|
||||
".hasOfficePreview": string;
|
||||
".hasBxf": string;
|
||||
".hasGlb": string;
|
||||
".hasHtml": string;
|
||||
".ctag": string;
|
||||
".etag": string;
|
||||
}
|
||||
|
||||
export interface IEditor {
|
||||
id: string;
|
||||
title: string;
|
||||
email: string;
|
||||
sip: string;
|
||||
picture: string;
|
||||
}
|
||||
|
||||
export interface IListSchema {
|
||||
Field: { [key: string]: string }[];
|
||||
RequiredFields: any[];
|
||||
JSLink: any[];
|
||||
LCID: string;
|
||||
Userid: string;
|
||||
PagePath: string;
|
||||
ShowWebPart: string;
|
||||
View: string;
|
||||
RootFolderParam: string;
|
||||
FieldSortParam: string;
|
||||
HttpVDir: string;
|
||||
IsDocLib: string;
|
||||
UIVersion: string;
|
||||
NoListItem: string;
|
||||
NoListItemHowTo: string;
|
||||
DefaultItemOpen: string;
|
||||
ForceCheckout: string;
|
||||
Direction: string;
|
||||
TabularView: string;
|
||||
ItemCount: string;
|
||||
EffectivePresenceEnabled: string;
|
||||
PresenceAlt: string;
|
||||
UserDispUrl: string;
|
||||
SelectedID: string;
|
||||
ListRight_AddListItems: string;
|
||||
FolderRight_AddListItems: string;
|
||||
InplaceSearchEnabled: string;
|
||||
RenderViewSelectorPivotMenuAsync: string;
|
||||
ViewSelector_ViewParameters: string;
|
||||
RenderSaveAsNewViewButton: string;
|
||||
Toolbar: string;
|
||||
".accessToken": string;
|
||||
".driveAccessToken": string;
|
||||
".driveUrl": string;
|
||||
".driveAccessTokenV21": string;
|
||||
".driveUrlV21": string;
|
||||
".mediaBaseUrl": string;
|
||||
".mediaBaseUrlSecondary": string;
|
||||
".pushChannelBaseUrl": string;
|
||||
".callerStack": string;
|
||||
".correlationId": string;
|
||||
".transformUrl": string;
|
||||
".thumbnailUrl": string;
|
||||
".videoManifestUrl": string;
|
||||
".pdfConversionUrl": string;
|
||||
".officeBundleGenerate": string;
|
||||
".officeBundleGetFragment": string;
|
||||
}
|
||||
|
||||
export interface IParentInfo {
|
||||
ParentFolderInfo: IParentFolderInfo[];
|
||||
}
|
||||
|
||||
export interface IParentFolderInfo {
|
||||
ServerRelativeUrl: string;
|
||||
Permissions: string;
|
||||
}
|
||||
|
||||
export interface ITemplates {
|
||||
}
|
||||
|
||||
export interface IViewMetadata {
|
||||
Id: string;
|
||||
ListViewXml: string;
|
||||
Paged: boolean;
|
||||
RowLimit: number;
|
||||
ServerRelativeUrl: string;
|
||||
Title: string;
|
||||
TabularView: boolean;
|
||||
ViewType: string;
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
// PnP
|
||||
import { sp, RenderListDataOptions } from "@pnp/sp";
|
||||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
|
||||
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
|
||||
import { IGetListDataAsStreamResult, IRow } from './IGetListDataAsStreamResult';
|
||||
import { GetAbsoluteDomainUrl } from "../../CommonUtils";
|
||||
|
||||
export class OneDriveServices {
|
||||
|
||||
private _oneDriveUrl: string = undefined;
|
||||
private _oneDriveFullUrl: string = undefined;
|
||||
|
||||
private _context: WebPartContext = undefined;
|
||||
private _absoluteUrl: string = undefined;
|
||||
private _serverRelativeFolderUrl: string = undefined;
|
||||
|
||||
private _accepts: string = undefined;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(context: WebPartContext, accepts: string) {
|
||||
this._context = context;
|
||||
this._accepts = accepts;
|
||||
|
||||
this._absoluteUrl = this._context.pageContext.web.absoluteUrl;
|
||||
|
||||
sp.setup({
|
||||
sp: { baseUrl: this._absoluteUrl }
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private _getOneDriveRootFolder = (): Promise<string> => {
|
||||
return sp.profiles.userProfile.then((currentUser) => {
|
||||
// Get the current user's personal site URL
|
||||
this._oneDriveUrl = currentUser.FollowPersonalSiteUrl;
|
||||
|
||||
// Get the list of ... uh.. lists on the user's personal site
|
||||
// BaseTemplate 700 and BaseType 1 means document library
|
||||
const apiUrl: string = `${this._absoluteUrl}/_api/SP.RemoteWeb(@a1)/Web/Lists?$filter=BaseTemplate eq 700 and BaseType eq 1&@a1='${encodeURIComponent(this._oneDriveUrl)}'`;
|
||||
|
||||
return this._context.spHttpClient.get(apiUrl,
|
||||
SPHttpClient.configurations.v1)
|
||||
.then((response: SPHttpClientResponse) => {
|
||||
return response.json().then((responseJSON: any) => {
|
||||
|
||||
// Get the first library
|
||||
const myDocumentsLibrary = responseJSON.value[0];
|
||||
|
||||
// Get the parent url
|
||||
const parentWebUrl: string = myDocumentsLibrary.ParentWebUrl;
|
||||
|
||||
// Get the first root folder. Assumed it is the same name as the library. Could be wrong.
|
||||
const serverRelativeRootFolder: string = `${myDocumentsLibrary.ParentWebUrl}/${myDocumentsLibrary.Title}`;
|
||||
|
||||
// Build an absolute URL so that we can refer to it
|
||||
this._oneDriveFullUrl = this._buildOneDriveAbsoluteUrl(serverRelativeRootFolder);
|
||||
|
||||
return serverRelativeRootFolder;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public GetListDataAsStream(rootFolder?: string) {
|
||||
// If we don't know what the root OneDrive folder is
|
||||
if (this._serverRelativeFolderUrl === undefined) {
|
||||
// Get the user's OneDrive root folder
|
||||
return this._getOneDriveRootFolder().then((oneDriveRootFolder: string) => {
|
||||
// Call the OneDrive root folder or whatever we passed in as root folder
|
||||
return this._getListDataAsStream(rootFolder ? rootFolder : oneDriveRootFolder);
|
||||
});
|
||||
} else {
|
||||
return this._getListDataAsStream(rootFolder ? rootFolder : this._serverRelativeFolderUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private _getListDataAsStream = (rootFolder: string): Promise<IGetListDataAsStreamResult> => {
|
||||
const listFullUrl: string = this._oneDriveFullUrl;
|
||||
const encodedFullUrl: string = encodeURIComponent(`'${listFullUrl}'`);
|
||||
const encodedRootFolder: string = encodeURIComponent(rootFolder);
|
||||
const listItemUrl: string = `${this._absoluteUrl}/_api/SP.List.GetListDataAsStream?listFullUrl=${encodedFullUrl}&View=&RootFolder=${encodedRootFolder}`;
|
||||
|
||||
const fileFilter: string = OneDriveServices.GetFileTypeFilter(this._accepts);
|
||||
const data: string = JSON.stringify({
|
||||
parameters: {
|
||||
RenderOptions: RenderListDataOptions.ContextInfo | RenderListDataOptions.ListData | RenderListDataOptions.ListSchema | RenderListDataOptions.ViewMetadata | RenderListDataOptions.EnableMediaTAUrls | RenderListDataOptions.ParentInfo,//4231, //4103, //4231, //192, //64
|
||||
AllowMultipleValueFilterForTaxonomyFields: true,
|
||||
ViewXml:
|
||||
`<View>
|
||||
<Query>
|
||||
<Where>
|
||||
<Or>
|
||||
<And>
|
||||
<Eq>
|
||||
<FieldRef Name="FSObjType" />
|
||||
<Value Type="Text">1</Value>
|
||||
</Eq>
|
||||
<Eq>
|
||||
<FieldRef Name="SortBehavior" />
|
||||
<Value Type="Text">1</Value>
|
||||
</Eq>
|
||||
</And>
|
||||
<In>
|
||||
<FieldRef Name="File_x0020_Type" />
|
||||
${fileFilter}
|
||||
</In>
|
||||
</Or>
|
||||
</Where>
|
||||
</Query>
|
||||
<ViewFields>
|
||||
<FieldRef Name="DocIcon"/>
|
||||
<FieldRef Name="LinkFilename"/>
|
||||
<FieldRef Name="Modified"/>
|
||||
<FieldRef Name="Editor"/>
|
||||
<FieldRef Name="FileSizeDisplay"/>
|
||||
<FieldRef Name="SharedWith"/>
|
||||
<FieldRef Name="MediaServiceFastMetadata"/>
|
||||
<FieldRef Name="MediaServiceOCR"/>
|
||||
<FieldRef Name="_ip_UnifiedCompliancePolicyUIAction"/>
|
||||
<FieldRef Name="ItemChildCount"/>
|
||||
<FieldRef Name="FolderChildCount"/>
|
||||
<FieldRef Name="SMTotalFileCount"/>
|
||||
<FieldRef Name="SMTotalSize"/>
|
||||
</ViewFields>
|
||||
<RowLimit Paged="TRUE">100</RowLimit>
|
||||
</View>`
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const spOpts: ISPHttpClientOptions = {
|
||||
method: "POST",
|
||||
body: data
|
||||
};
|
||||
|
||||
return this._context.spHttpClient.fetch(listItemUrl, SPHttpClient.configurations.v1, spOpts)
|
||||
.then((listResponse: SPHttpClientResponse) => listResponse.json().then((listResponseJSON: IGetListDataAsStreamResult) => listResponseJSON));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an absolute URL
|
||||
*/
|
||||
private _buildOneDriveAbsoluteUrl = (relativeUrl: string) => {
|
||||
const siteUrl: string = GetAbsoluteDomainUrl(this._oneDriveUrl);
|
||||
return siteUrl + relativeUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a file filter
|
||||
*/
|
||||
public static GetFileTypeFilter(accepts: string) {
|
||||
let fileFilter: string = undefined;
|
||||
fileFilter = "<Values>";
|
||||
accepts.split(",").forEach((fileType: string, index: number) => {
|
||||
fileType = fileType.replace(".", "");
|
||||
if (index > 0) {
|
||||
fileFilter = fileFilter + `<Value Type="Text">${fileType}</Value>`;
|
||||
}
|
||||
});
|
||||
fileFilter = fileFilter + "</Values>";
|
||||
|
||||
return fileFilter;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './OneDriveServices';
|
||||
export * from './IGetListDataAsStreamRequest';
|
||||
export * from './IGetListDataAsStreamResult';
|
||||
export * from './IDimensions';
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "245378e3-d4e2-480b-a32f-5508a8c398e3",
|
||||
"alias": "ComparerWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
"requiresCustomScript": false,
|
||||
"preconfiguredEntries": [
|
||||
{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
|
||||
"group": {
|
||||
"default": "Other"
|
||||
},
|
||||
"title": {
|
||||
"default": "Comparer"
|
||||
},
|
||||
"description": {
|
||||
"default": "Compare two images side-by-side using a slider."
|
||||
},
|
||||
"iconImageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAYCAYAAAAPtVbGAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAAeQAAAHkBOLWIEgAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAGySURBVEiJrdQ9aFRBFAXg7212NSDa2VpZaeEfRkQsVJBgnaCNP4iFWAh2gkIEbQQbCwUlxD4ERUUCgkLAXkRQFEEknX9goYUhPIuZxGF5LzvvsQcWhnfPuWfnzr23wGvsNBiPcQf70MVUhgZ+Q4mRTMEEbmVyoYey00DQGsMyGRX+dWuTo1jApZr4KfzCdxxpY7IZD2OCxRrOFNZhEy63MdmKDcIt5mo4X2rOq+gOMHkThQu4h08VnNO4IrTq9TYmf3AA5/Fe9eMuxvia6J+TAhexp4I7lDkZwTRu4wXGGiTcURdITbp4gBM4hmd4jr0Dkhe4KaynC3WkUuigJ0K/H0xM5/ADu7DF/3Jti5zRyCnjbxnnkty9+F2JlzFZf3l6eISfmE1Mrgkd9SoxSI3OpiYr3bUdh4WWTbGE47hfEbtRURXCE0xHg5mVksAhvKsR/cWZeJ6o4fSjwF3hVqsP/yFT3AQdYYCHtoXXMtLFR3zLEMwLTdAUS0U02phDxjj246rQvtkmuRjDbmGtf8bTBtpsnEzOk1ifK2zy8OmtO+Ik52DQqk/xVpiXZXwV5icL/wCnaVnpdL2nMAAAAABJRU5ErkJggg==",
|
||||
"properties": {
|
||||
"beforeLabel": "",
|
||||
"afterLabel": "",
|
||||
"startPosition": 50,
|
||||
"aspectRatio": "16:9"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
|
||||
// Needed for base SharePoint stuff
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneLabel,
|
||||
PropertyPaneSlider,
|
||||
PropertyPaneChoiceGroup,
|
||||
IPropertyPaneChoiceGroupOption,
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
//
|
||||
import { PropertyFieldSpinButton } from '@pnp/spfx-property-controls/lib/PropertyFieldSpinButton';
|
||||
|
||||
// Localized strings
|
||||
import * as strings from 'ComparerWebPartStrings';
|
||||
|
||||
// Used to load the Comparer component
|
||||
import Comparer from './components/Comparer';
|
||||
import { IComparerProps } from './components/Comparer.types';
|
||||
|
||||
// Needed to display a custom property pane file picker
|
||||
import { PropertyPaneFilePicker, ItemType } from '../../controls/PropertyPaneFilePicker';
|
||||
|
||||
export interface IComparerWebPartProps {
|
||||
afterImg: string;
|
||||
afterLabel: string;
|
||||
aspectRatio: '1:1' | '3:2' | '4:3' | '16:9';
|
||||
beforeImg: string;
|
||||
beforeLabel: string;
|
||||
startPosition: number;
|
||||
title: string;
|
||||
beforeAlternateText: string;
|
||||
afterAlternateText: string;
|
||||
}
|
||||
|
||||
const aspectRatio1_1: string = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAABECAYAAAA4E5OyAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjEuNWRHWFIAAADeSURBVHhe7dAxCsNAEARBvVP//8fZDgxi6GjBJ4seQSnoS5Y51lp1gdEMoxlGM4xmGM0wmmE0w2iG0QyjGUYzjGYYzTCaYTTDaIbRDKMZxon39/ndJu+ZwjiRB+6W90xhnPgedp7nVo8YJN9+pYOEDhI6SOggoYOEDhI6SOggoYOEDhI6SOggoYOEDhI6SOggoYOEDhI6SOggoYOEDhI6SOggoYOExwyy098Pcpe8ZwrjRB64W94zhdEMoxlGM4xmGM0wmmE0w2iG0QyjGUYzjGYYzTCaYTTDaIbRax0vGYi5boi8iG0AAAAASUVORK5CYII=`;
|
||||
|
||||
/**
|
||||
* Change the line below to true if you want to hide the Web search tab.
|
||||
*/
|
||||
const DISABLE_WEB_SEARCH_TAB: boolean = false;
|
||||
export default class ComparerWebPart extends BaseClientSideWebPart<IComparerWebPartProps> {
|
||||
|
||||
// protected onInit(): Promise<void> {
|
||||
// return new Promise<void>((resolve, _reject) => {
|
||||
|
||||
// if (this.properties.aspectRatio === undefined) {
|
||||
// this.properties.aspectRatio = '1:1';
|
||||
// }
|
||||
|
||||
// resolve(undefined);
|
||||
// });
|
||||
// }
|
||||
|
||||
public render(): void {
|
||||
// Get the width of this web part, we'll need it to resize the images
|
||||
const { clientWidth } = this.domElement;
|
||||
|
||||
// Calculate the aspect ratio
|
||||
let ratio: number = undefined;
|
||||
switch (this.properties.aspectRatio) {
|
||||
case "1:1":
|
||||
ratio = 1;
|
||||
break;
|
||||
case "4:3":
|
||||
ratio = 4 / 3;
|
||||
break;
|
||||
case "3:2":
|
||||
ratio = 3 / 2;
|
||||
break;
|
||||
case "16:9":
|
||||
default:
|
||||
ratio = 16 / 9;
|
||||
}
|
||||
|
||||
// Calculate the height based on the selected aspect ratio
|
||||
const calculatedHeight: number = clientWidth / ratio;
|
||||
|
||||
const element: React.ReactElement<IComparerProps> = React.createElement(
|
||||
Comparer,
|
||||
{
|
||||
afterImg: this.properties.afterImg,
|
||||
afterLabel: this.properties.afterLabel,
|
||||
beforeImg: this.properties.beforeImg,
|
||||
beforeLabel: this.properties.beforeLabel,
|
||||
displayMode: this.displayMode,
|
||||
height: calculatedHeight,
|
||||
onConfigure: this.context.propertyPane.open,
|
||||
startPosition: this.properties.startPosition,
|
||||
title: this.properties.title,
|
||||
width: clientWidth,
|
||||
beforeAlternateText: this.properties.beforeAlternateText,
|
||||
afterAlternateText: this.properties.afterAlternateText,
|
||||
onUpdateTitle: (value: string) => {
|
||||
// when title is changed, store the new title
|
||||
this.properties.title = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the web part when resized
|
||||
* @param _newWidth
|
||||
*/
|
||||
protected onAfterResize(_newWidth: number): void {
|
||||
// redraw the web part
|
||||
this.render();
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
displayGroupsAsAccordion: true,
|
||||
groups: [
|
||||
{
|
||||
|
||||
groupName: strings.BeforeImageGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneLabel('beforeImg', {
|
||||
text: strings.BeforeImageGroupDescription
|
||||
}),
|
||||
PropertyPaneFilePicker('beforeImg', {
|
||||
key: 'beforeImgId',
|
||||
label: strings.BeforeImageFieldLabel,
|
||||
buttonLabel: this.properties.beforeImg ? strings.BeforeImageChangeButtonLabel : strings.BeforeImageButtonLabel,
|
||||
value: this.properties.beforeImg,
|
||||
webPartContext: this.context,
|
||||
itemType: ItemType.Images,
|
||||
required: true,
|
||||
disableWebSearchTab: DISABLE_WEB_SEARCH_TAB,
|
||||
onSave: (value: string) => { this.properties.beforeImg = value; }
|
||||
}),
|
||||
PropertyPaneTextField('beforeLabel', {
|
||||
label: strings.BeforeImageLabelFieldLabel
|
||||
}),
|
||||
PropertyPaneTextField('beforeAlternateText', {
|
||||
label: strings.BeforeImageAlternateTextFieldLabel,
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
description: strings.AlternateTextFieldDescription
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.AfterImageGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneLabel('afterImg', {
|
||||
text: strings.AfterImageGroupDescription
|
||||
}),
|
||||
PropertyPaneFilePicker('afterImg', {
|
||||
key: 'afterImgId',
|
||||
label: strings.AfterImageFieldLabel,
|
||||
buttonLabel: this.properties.afterImg ? strings.AfterImageChangeButtonLabel : strings.AfterImageButtonLabel,
|
||||
value: this.properties.afterImg,
|
||||
webPartContext: this.context,
|
||||
itemType: ItemType.Images,
|
||||
required: true,
|
||||
disableWebSearchTab: DISABLE_WEB_SEARCH_TAB,
|
||||
onSave: (value: string) => { this.properties.afterImg = value; }
|
||||
}),
|
||||
PropertyPaneTextField('afterLabel', {
|
||||
label: strings.AfterImageLabelFieldLabel,
|
||||
}),
|
||||
PropertyPaneTextField('afterAlternateText', {
|
||||
label: strings.AfterImageAlternateTextFieldLabel,
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
description: strings.AlternateTextFieldDescription
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.OptionsGroupName,
|
||||
isCollapsed: true,
|
||||
groupFields: [
|
||||
PropertyPaneSlider('startPosition', {
|
||||
label: strings.StartPositionFieldLabel,
|
||||
min: 0,
|
||||
max: 100,
|
||||
showValue: true,
|
||||
step: 1
|
||||
}),
|
||||
PropertyPaneChoiceGroup('aspectRatio', {
|
||||
label: strings.AspectRatioFieldLabel,
|
||||
options: [
|
||||
{
|
||||
key: '1:1',
|
||||
text: '1:1',
|
||||
selectedImageSrc: require('./assets/AspectRatio1_1.png'),
|
||||
imageSrc: require('./assets/AspectRatio1_1.png'),
|
||||
},
|
||||
{
|
||||
key: '3:2',
|
||||
text: '3:2',
|
||||
selectedImageSrc: require('./assets/AspectRatio3_2.png'),
|
||||
imageSrc: require('./assets/AspectRatio3_2.png'),
|
||||
},
|
||||
{
|
||||
key: '4:3',
|
||||
text: '4:3',
|
||||
selectedImageSrc: require('./assets/AspectRatio4_3.png'),
|
||||
imageSrc: require('./assets/AspectRatio4_3.png'),
|
||||
},
|
||||
{
|
||||
key: '16:9',
|
||||
text: '16:9',
|
||||
selectedImageSrc: require('./assets/AspectRatio16_9.png'),
|
||||
imageSrc: require('./assets/AspectRatio16_9.png'),
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 380 B |
After Width: | Height: | Size: 365 B |
After Width: | Height: | Size: 381 B |
After Width: | Height: | Size: 381 B |
|
@ -0,0 +1,88 @@
|
|||
@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss";
|
||||
|
||||
.comparer {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $ms-color-neutralTertiaryAlt;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
// Darken the image at the bottom if we have labels, for accessibility reasons.
|
||||
.withLabel {
|
||||
&::after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0) linear-gradient(to bottom, transparent 0px, $ms-color-blackTranslucent40 100%) repeat 0
|
||||
0;
|
||||
top: 50%;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.comparerSlider {
|
||||
background-color: $ms-color-neutralSecondary;
|
||||
cursor: col-resize;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.comparerSlider:after {
|
||||
background-color: $ms-color-white;
|
||||
border-radius: 50%;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: $ms-color-neutralSecondary;
|
||||
bottom: -12px;
|
||||
content: "";
|
||||
height: 18px;
|
||||
left: -9px;
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.comparerBefore,
|
||||
.comparerAfter {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.afterWrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparerLabel {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
color: $ms-color-white;
|
||||
padding-left: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-top: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding-bottom: 28px;
|
||||
z-index: 25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparerLabelBefore {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.comparerLabelAfter {
|
||||
left: 0;
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Styles
|
||||
import styles from './Comparer.module.scss';
|
||||
|
||||
// Our props and state
|
||||
import { IComparerProps, IComparerState } from './Comparer.types';
|
||||
|
||||
// Used to make a draggable slider
|
||||
import Draggable, { DraggableData } from 'react-draggable';
|
||||
|
||||
// Used for dynamic CSS classname
|
||||
import { css } from "@uifabric/utilities/lib/css";
|
||||
|
||||
// Used to create an image that covers the entire area
|
||||
import BlockImage from 'react-block-image';
|
||||
|
||||
// Localization
|
||||
import * as strings from 'ComparerWebPartStrings';
|
||||
|
||||
// Needed to know if the web part is editable and show the title control
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
|
||||
// PnP controls rock!
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
|
||||
export default class Comparer extends React.Component<IComparerProps, IComparerState> {
|
||||
/**
|
||||
* Keep track of the labels so we can retrieve their positions
|
||||
*/
|
||||
private _beforeLabelElem: HTMLDivElement = undefined;
|
||||
private _afterLabelElem: HTMLDivElement = undefined;
|
||||
|
||||
/**
|
||||
* Keep track of the top parent so we can get relative positions
|
||||
*/
|
||||
private _topElem: HTMLDivElement = undefined;
|
||||
constructor(props: IComparerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sliderPositionX: (this.props.startPosition / 100 * this.props.width),
|
||||
sliderPosition: (this.props.startPosition),
|
||||
showChooseImagePanel: false,
|
||||
hasLabels: this._checkHasLabels(this.props)
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IComparerProps, prevState: IComparerState): void {
|
||||
// If we updated the slider position, forget the current state and recalculate position
|
||||
if (prevProps.startPosition !== this.props.startPosition) {
|
||||
this.setState({
|
||||
sliderPositionX: (this.props.startPosition / 100 * this.props.width),
|
||||
sliderPosition: (this.props.startPosition),
|
||||
});
|
||||
}
|
||||
|
||||
// If we added/removed labels, change the state
|
||||
const hasLabels: boolean = this._checkHasLabels(this.props);
|
||||
if (this._checkHasLabels(prevProps) !== hasLabels) {
|
||||
this.setState({
|
||||
hasLabels: hasLabels
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IComparerProps> {
|
||||
const {
|
||||
beforeImg,
|
||||
afterImg,
|
||||
className,
|
||||
beforeClassName,
|
||||
afterClassName,
|
||||
handleClassName,
|
||||
width,
|
||||
height
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sliderPosition,
|
||||
hasLabels
|
||||
} = this.state;
|
||||
|
||||
if (!this.props.beforeImg || !this.props.afterImg) {
|
||||
return this._renderPlaceholders();
|
||||
}
|
||||
const sliderPositionX: number = (sliderPosition / 100 * this.props.width);
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<WebPartTitle displayMode={this.props.displayMode}
|
||||
title={this.props.title}
|
||||
updateProperty={this.props.onUpdateTitle} />
|
||||
<div
|
||||
className={css(styles.comparer, hasLabels ? styles.withLabel : undefined, className)}
|
||||
style={{
|
||||
width,
|
||||
height
|
||||
}}
|
||||
ref={this._linkTop}
|
||||
>
|
||||
|
||||
<Draggable
|
||||
bounds={"parent"}
|
||||
disabled={false}
|
||||
allowAnyClick={false}
|
||||
enableUserSelectHack={true}
|
||||
axis={"x"}
|
||||
defaultClassName={styles.comparerSlider}
|
||||
defaultPosition={{ x: sliderPositionX, y: 0 }}
|
||||
position={{ x: sliderPositionX, y: 0 }}
|
||||
onDrag={(e, data: DraggableData) => this._handleDrag(e, data)}>
|
||||
<div className={handleClassName}></div>
|
||||
</Draggable>
|
||||
<div
|
||||
className={styles.afterWrapper}
|
||||
style={{
|
||||
width: `${sliderPositionX}px`
|
||||
}}
|
||||
>
|
||||
<BlockImage
|
||||
role={"Image"}
|
||||
aria-label={this.props.afterAlternateText}
|
||||
src={afterImg}
|
||||
className={css(styles.comparerAfter, afterClassName)}
|
||||
style={{ width }}
|
||||
/>
|
||||
<div
|
||||
className={css(styles.comparerLabel, styles.comparerLabelAfter)}
|
||||
ref={this._linkAfterLabel}>{this.props.afterLabel}</div>
|
||||
</div>
|
||||
<BlockImage
|
||||
role={"Image"}
|
||||
aria-label={this.props.beforeAlternateText}
|
||||
src={beforeImg}
|
||||
className={css(styles.comparerBefore, beforeClassName)}
|
||||
/>
|
||||
<div
|
||||
className={css(styles.comparerLabel, styles.comparerLabelBefore)}
|
||||
ref={this._linkBeforeLabel}>{this.props.beforeLabel}</div>
|
||||
</div>
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the web part isn't configured, renders a placeholder
|
||||
*/
|
||||
private _renderPlaceholders = (): JSX.Element => {
|
||||
const {
|
||||
onConfigure
|
||||
} = this.props;
|
||||
|
||||
if (this.props.displayMode === DisplayMode.Edit) {
|
||||
return <div
|
||||
className={styles.comparer}>
|
||||
<Placeholder iconName='PhotoCollection'
|
||||
iconText={strings.PlaceholderIconText}
|
||||
description={strings.PlaceholderDescription}
|
||||
buttonLabel={strings.PlaceholderButtonLabel}
|
||||
onConfigure={onConfigure} />
|
||||
</div>;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if labels were specified or now
|
||||
*/
|
||||
private _checkHasLabels = (props: IComparerProps): boolean => {
|
||||
return props.beforeLabel!.length > 0 || props.afterLabel!.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user drags the slider
|
||||
*/
|
||||
private _handleDrag(_e, data: DraggableData) {
|
||||
const { sliderPositionX } = this.state;
|
||||
const { width } = this.props;
|
||||
|
||||
this.setState({
|
||||
sliderPositionX: data.x,
|
||||
sliderPosition: sliderPositionX / width * 100
|
||||
});
|
||||
|
||||
// If we have no labels, don't do anything else
|
||||
if (!this.state.hasLabels) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We DO have labels, we need to move some labels, and maybe hide them
|
||||
// otherwise they get squished when the slider goes too far to the left or right
|
||||
|
||||
// Get the position of the right label and see if we need to hide it
|
||||
const parentPos: ClientRect = this._topElem.getBoundingClientRect();
|
||||
const labelPos: ClientRect = this._beforeLabelElem.getBoundingClientRect();
|
||||
const labelX: number = labelPos.left - parentPos.left;
|
||||
|
||||
// Is the slider past the label?
|
||||
if (labelX < data.x) {
|
||||
// Hide the label
|
||||
this._beforeLabelElem.style.opacity = "0";
|
||||
}
|
||||
else {
|
||||
// Show the label
|
||||
this._beforeLabelElem.style.opacity = "1";
|
||||
}
|
||||
|
||||
// Get the right-most position of the left label
|
||||
const afterLabelPos: ClientRect = this._afterLabelElem.getBoundingClientRect();
|
||||
const afterLabelX: number = afterLabelPos.left - parentPos.left + afterLabelPos.width;
|
||||
|
||||
// If the slider is to the left of the label
|
||||
if (data.x < afterLabelX) {
|
||||
// Hide it
|
||||
this._afterLabelElem.style.opacity = "0";
|
||||
}
|
||||
else {
|
||||
// Show it
|
||||
this._afterLabelElem.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* link to the top element
|
||||
*/
|
||||
private _linkTop = (e: any) => {
|
||||
this._topElem = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to the right label
|
||||
*/
|
||||
private _linkBeforeLabel = (e: any) => {
|
||||
this._beforeLabelElem = e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to the left label
|
||||
*/
|
||||
private _linkAfterLabel = (e: any) => {
|
||||
this._afterLabelElem = e;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
|
||||
export interface IComparerProps {
|
||||
afterClassName?: string;
|
||||
afterImg: string;
|
||||
afterLabel: string;
|
||||
beforeClassName?: string;
|
||||
beforeImg: string;
|
||||
beforeLabel: string;
|
||||
className?: string;
|
||||
displayMode: DisplayMode;
|
||||
handleClassName?: string;
|
||||
height: number;
|
||||
startPosition: number;
|
||||
title: string;
|
||||
width: number;
|
||||
beforeAlternateText: string;
|
||||
afterAlternateText: string;
|
||||
onConfigure: () => void;
|
||||
onUpdateTitle: (value: string) => void;
|
||||
}
|
||||
|
||||
export interface IComparerState {
|
||||
sliderPosition: number;
|
||||
sliderPositionX: number;
|
||||
showChooseImagePanel: boolean;
|
||||
hasLabels: boolean;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"AfterImageAlternateTextFieldLabel": "Alternative text",
|
||||
"AfterImageButtonLabel": "Choose image",
|
||||
"AfterImageChangeButtonLabel": "Change image",
|
||||
"AfterImageFieldLabel": "Image",
|
||||
"AfterImageGroupName": "After image",
|
||||
"AfterImageGroupDescription": "The After image appears on the left of the comparer. It is revealed as users drag the divider to the right.",
|
||||
"AfterImageLabelFieldLabel": "Label",
|
||||
"AlternateTextFieldDescription": "Describe this image for people who can't see it",
|
||||
"AlternateTextLabel": "Provide alternative text to describe this image. Alternative text helps people with screen readers understand the content of pictures.",
|
||||
"BeforeImageAlternateTextFieldLabel": "Alternative text",
|
||||
"BeforeImageButtonLabel": "Choose image",
|
||||
"BeforeImageChangeButtonLabel": "Change image",
|
||||
"BeforeImageFieldLabel": "Image",
|
||||
"BeforeImageGroupDescription": "The Before image appears on the right of the comparer. It covers the After image as users drag the divider to the left.",
|
||||
"BeforeImageGroupName": "Before image",
|
||||
"BeforeImageLabelFieldLabel": "Label",
|
||||
"OptionsGroupName": "Options",
|
||||
"PlaceholderButtonLabel": "Add images",
|
||||
"PlaceholderDescription": "In order to compare images, you need to insert a 'Before' and an 'After' image.",
|
||||
"PlaceholderIconText": "Compare images",
|
||||
"PropertyPaneDescription": "Select a Before and After image. Add text labels over your images and description. Choose options such as starting position and web part height.",
|
||||
"StartPositionFieldLabel": "Start position (%)",
|
||||
"AspectRatioFieldLabel": "Aspect ratio",
|
||||
}
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
declare interface IComparerWebPartStrings {
|
||||
|
||||
AfterImageAlternateTextFieldLabel: string;
|
||||
AfterImageButtonLabel: string;
|
||||
AfterImageChangeButtonLabel: string;
|
||||
AfterImageFieldLabel: string;
|
||||
AfterImageGroupName: string;
|
||||
AfterImageGroupDescription: string;
|
||||
AfterImageLabelFieldLabel: string;
|
||||
AlternateTextFieldDescription: string;
|
||||
BeforeImageAlternateTextFieldLabel: string;
|
||||
BeforeImageButtonLabel: string;
|
||||
BeforeImageChangeButtonLabel: string;
|
||||
BeforeImageFieldLabel: string;
|
||||
BeforeImageGroupDescription: string;
|
||||
BeforeImageGroupName: string;
|
||||
BeforeImageLabelFieldLabel: string;
|
||||
OptionsGroupName: string;
|
||||
PlaceholderButtonLabel: string;
|
||||
PlaceholderDescription: string;
|
||||
PlaceholderIconText: string;
|
||||
PropertyPaneDescription: string;
|
||||
StartPositionFieldLabel: string;
|
||||
AspectRatioFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'ComparerWebPartStrings' {
|
||||
const strings: IComparerWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": { "*": ["types/*"] },
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts", "src/controls/PropertyPaneFilePicker/WebSearchTab/SearchResultTile.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
|
@ -54,8 +54,10 @@ Property Bag Navigation Webparts<br/>[react-property-bag-editor](https://github.
|
|||
Provision SharePoint Assets with the SPFx solution package<br/>[react-provision-assets](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-provision-assets)|This sample shows how we can provision Document Library, Custom List, Web and List PropertyBag properties, Site Columns, Content Types, Images, Site Page with the SFPx Client side webpart and even prepopulated list and library items along with the SPFx solution package. All of the components can be deployed at once with the SPFx webpart when the app is added to a SharePoint site. It also contains custom list and document library xml schemas.|![react-provision-assets](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-provision-assets/assets/spfx-provision-assets.gif)|![drop](https://img.shields.io/badge/drop-GA-green.svg)
|
||||
React & Office Graph Web Part samples<br/>[react-officegraph](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-officegraph)|Sample SharePoint Framework Client-Side Web Parts built using React showing interacting with the Office Graph.|![react-officegraph](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-officegraph/assets/trendinginthissite-preview.png)|![drop](https://img.shields.io/badge/drop-ga-green.svg)
|
||||
React Aggregated Calendar Webpart<br/>[react-aggregated-calendar](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-aggregated-calendar)|This is a sample webpart developed using React Framework to gather the aggregated events from the multiple calendars from multiple sites using Full Calendar from fullcalendar.io|![react-aggregated-calendar](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-aggregated-calendar/assets/react-aggregated-calendar.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
React Birthdays Web Part<br/>[react-birthdays](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-birthdays)|The Web Part Birthdays shows the upcoming birthdays in the company, the web part reads birthdays from a list located on the tenant's home site with title "Birthdays."|![react-birthdays](https://github.com/SharePoint/sp-dev-fx-webparts/raw/master/samples/react-birthdays/assets/birthdays.png)|![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
React Calendar Feed Web Part<br/>[react-calendar-feed](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-calendar-feed)|This web part uses RSS event feeds, iCal feeds, or WordPress calendar feeds and renders events using a look and feel that is consistent with the SharePoint out-of-the-box Group calendar/events web part.|![react-calendar-feed](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-calendar-feed/assets/react-calendar-feed-demo.gif)|![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
|
||||
React Chart Control<br/>[react-chartcontrol](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-chartcontrol)|This sample contains several web parts that demonstrate how to use the ChartControl from @pnp/spfx-controls-react.|![react-chartcontrol](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-chartcontrol/assets/react-chartcontrol.gif)|![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
|
||||
React Chart Control<br/>[react-chartcontrol](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-chartcontrol)|This sample contains several web parts that demonstrate how to use the ChartControl from @pnp/spfx-controls-react.|![react-chartcontrol](https://github.com/SharePoint/sp-dev-fx-webparts/raw/master/samples/react-chartcontrol/assets/WebPartList.png)|![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
|
||||
React Comparer<br/>[react-comparer](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-comparer)|Allows users to compare Before and After pictures, with a drag-and-drop slider. Implements a custom file picker.|![react-comparer](https://github.com/SharePoint/sp-dev-fx-webparts/raw/master/samples/react-comparer/assets/ComparerWebPart.png)|![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
|
||||
React Content Query WebPart<br/>[react-content-query-webpart](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-content-query-webpart)|The React Content Query WebPart is a modern version of the good old Content by Query WebPart that was introduced in SharePoint 2007. Built for SharePoint 2016 and Office 365, this modern version is built against the new SharePoint Framework (SPFx) and uses the latest Web Stack practices. While the original WebPart was based on a XSLT templating engine, this React WebPart is based on the well known Handlebars templating engine, which empowers users to create simple, yet powerfull HTML templates for rendering the queried content. This new version also lets the user query any site collections which resides on the same domain url, add unlimited filters, query DateTime fields to the nearest minute rather than being limited to a day, and much more.|![react-content-query-webpart](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-content-query-webpart/Misc/toolpart.gif)|![drop](https://img.shields.io/badge/drop-GA-green.svg)
|
||||
React File Upload WebPart<br/>[react-file-upload](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-file-upload)|The file upload web part allowing users to upload multiple files to a document library or as item attachments.|![react-file-upload](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-file-upload/assets/SPFileUploadPreview.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
React List Form WebPart<br/>[react-list-form](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-list-form)|The React List Form web part is a web part for adding a list form to any page. It provides a working example of implementing generic SharePoint list forms using the SharePoint Framework (SPFx) and the React and Office UI Fabric libraries.|![react-list-form](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-list-form/assets/React-ListForm-Overview.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
|
@ -82,6 +84,7 @@ SharePoint CRUD operations<br/>[sharepoint-crud](https://github.com/SharePoint/s
|
|||
SharePoint Framework Facebook Page Social Plugin web part sample<br/>[react-facebook-plugin](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-facebook-plugin)|This sample shows how to implement iFrame-based web parts with a dynamic responsive behavior on the example of Facebook Page Social Plugin.|![react-facebook-plugin](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-facebook-plugin/assets/preview.png)|![drop](https://img.shields.io/badge/drop-1.5.1-blue.svg)
|
||||
SharePoint Framework PnP Controls Sample<br/>[pnp-controls](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/pnp-controls)|This is a sample project that contains a web part which makes use of the PnP SPFx Controls:|![pnp-controls](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/pnp-controls/assets/webpart-outcome.gif)|![drop](https://img.shields.io/badge/drop-1.4.1-green.svg)
|
||||
SharePoint Framework React Jest Testing sample<br/>[react-jest-testing](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-jest-testing)|This sample uses the popular Jest Testing Framework with a SPFx client side solution. It is a SPFx-Jest-Enzyme-Sinon starter kit so you can start writing and debugging unit tests in typescript for your SPFx solution.The setup includes unit tests examples, code coverage reports in different formats, visual studio code unit test debug configurations for typescript, setting a coverage threshold (gates) for continuous integration and continuous deployment scenarios.|![react-jest-testing](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-jest-testing/assets/Jest-Typescript-VSCode-debugging.png)|![drop](https://img.shields.io/badge/drop-1.6.0-green.svg)
|
||||
SharePoint Framework RSS Reader<br/>[react-rss-reader](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-rss-reader)|A RSS Reader original based on work completed by Olivier Carpentier's from: https://github.com/OlivierCC/spfx-40-fantastics/tree/master/src/webparts/rssReader Root project: https://github.com/OlivierCC/spfx-40-fantastics| ![react-rss-reader](https://github.com/SharePoint/sp-dev-fx-webparts/raw/cfe16c906678ea36e79065844b61bc2888305abd/samples/react-rss-reader/images/react-rss-reader.gif) |![drop](https://img.shields.io/badge/drop-1.7.0-green.svg)
|
||||
SharePoint Framework search with search box, refiners and paging sample<br/>[react-search-refiners](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-search-refiners)|This sample shows you how to build user friendly SharePoint search experiences using SPFx in the modern interface. The main features include:|![react-search-refiners](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-search-refiners/images/react-search-refiners.gif)|![drop](https://img.shields.io/badge/drop-1.7.0-green.svg)
|
||||
SharePoint Framework webpart sample using React, Redux and ImmutableJS<br/>[react-redux-async-immutablejs](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-redux-async-immutablejs)|SharePoint Framework webpart which uses Redux to maintain a single state for the entire application and ImmutableJS to create performant state trees.|![react-redux-async-immutablejs](https://raw.githubusercontent.com/vman/sp-dev-fx-webparts/master/samples/react-redux-async-immutablejs/assets/react-redux-immutable.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
SharePoint Themes Client Side Web Part<br/>[react-themes](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-themes)|This web part illustrates how to use SharePoint Theme variables in custom web parts.|![react-themes](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-themes/assets/themes.png)|![drop](https://img.shields.io/badge/drop-ga-green.svg)
|
||||
|
|