From c0ac645a44a10e1f50465c6f6fd817f807a591f8 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 13 Sep 2017 10:23:39 +0200 Subject: [PATCH] Initial commit of the react components --- .editorconfig | 25 ++ .gitattributes | 1 + .gitignore | 65 ++--- .npmignore | 22 ++ .yo-rc.json | 8 + README.md | 5 + config/config.json | 17 ++ config/copy-assets.json | 4 + config/deploy-azure-storage.json | 7 + config/package-solution.json | 12 + config/serve.json | 10 + config/tslint.json | 45 ++++ config/write-manifests.json | 4 + gulpfile.js | 6 + package.json | 33 +++ src/FileTypeIcon.ts | 1 + src/ListView.ts | 1 + src/Placeholder.ts | 1 + src/SiteBreadcrumb.ts | 1 + src/controls/fileTypeIcon/FileTypeIcon.tsx | 222 +++++++++++++++++ src/controls/fileTypeIcon/IFileTypeIcon.ts | 213 ++++++++++++++++ src/controls/fileTypeIcon/index.ts | 2 + src/controls/listView/IListView.ts | 72 ++++++ src/controls/listView/ListView.tsx | 233 ++++++++++++++++++ src/controls/listView/index.ts | 2 + .../placeholder/IPlaceholderComponent.ts | 34 +++ .../PlaceholderComponent.module.scss | 68 +++++ .../placeholder/PlaceholderComponent.tsx | 61 +++++ src/controls/placeholder/index.ts | 2 + .../siteBreadcrumb/ISiteBreadcrumb.ts | 18 ++ .../siteBreadcrumb/SiteBreadcrumb.module.scss | 25 ++ .../siteBreadcrumb/SiteBreadcrumb.tsx | 129 ++++++++++ src/controls/siteBreadcrumb/index.ts | 2 + src/loc/en-us.js | 5 + src/loc/mystrings.d.ts | 8 + .../ControlsTestWebPart.manifest.json | 32 +++ .../controlsTest/ControlsTestWebPart.ts | 54 ++++ .../controlsTest/IControlsTestWebPartProps.ts | 3 + .../components/ControlsTest.module.scss | 44 ++++ .../controlsTest/components/ControlsTest.tsx | 163 ++++++++++++ .../components/IControlsTestProps.ts | 12 + src/webparts/controlsTest/loc/en-us.js | 7 + src/webparts/controlsTest/loc/mystrings.d.ts | 10 + .../test/ControlsTestWebPart.test.ts | 9 + tsconfig.json | 16 ++ typings/@ms/odsp.d.ts | 11 + typings/tsd.d.ts | 1 + 47 files changed, 1683 insertions(+), 43 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .npmignore create mode 100644 .yo-rc.json create mode 100644 config/config.json create mode 100644 config/copy-assets.json create mode 100644 config/deploy-azure-storage.json create mode 100644 config/package-solution.json create mode 100644 config/serve.json create mode 100644 config/tslint.json create mode 100644 config/write-manifests.json create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/FileTypeIcon.ts create mode 100644 src/ListView.ts create mode 100644 src/Placeholder.ts create mode 100644 src/SiteBreadcrumb.ts create mode 100644 src/controls/fileTypeIcon/FileTypeIcon.tsx create mode 100644 src/controls/fileTypeIcon/IFileTypeIcon.ts create mode 100644 src/controls/fileTypeIcon/index.ts create mode 100644 src/controls/listView/IListView.ts create mode 100644 src/controls/listView/ListView.tsx create mode 100644 src/controls/listView/index.ts create mode 100644 src/controls/placeholder/IPlaceholderComponent.ts create mode 100644 src/controls/placeholder/PlaceholderComponent.module.scss create mode 100644 src/controls/placeholder/PlaceholderComponent.tsx create mode 100644 src/controls/placeholder/index.ts create mode 100644 src/controls/siteBreadcrumb/ISiteBreadcrumb.ts create mode 100644 src/controls/siteBreadcrumb/SiteBreadcrumb.module.scss create mode 100644 src/controls/siteBreadcrumb/SiteBreadcrumb.tsx create mode 100644 src/controls/siteBreadcrumb/index.ts create mode 100644 src/loc/en-us.js create mode 100644 src/loc/mystrings.d.ts create mode 100644 src/webparts/controlsTest/ControlsTestWebPart.manifest.json create mode 100644 src/webparts/controlsTest/ControlsTestWebPart.ts create mode 100644 src/webparts/controlsTest/IControlsTestWebPartProps.ts create mode 100644 src/webparts/controlsTest/components/ControlsTest.module.scss create mode 100644 src/webparts/controlsTest/components/ControlsTest.tsx create mode 100644 src/webparts/controlsTest/components/IControlsTestProps.ts create mode 100644 src/webparts/controlsTest/loc/en-us.js create mode 100644 src/webparts/controlsTest/loc/mystrings.d.ts create mode 100644 src/webparts/controlsTest/test/ControlsTestWebPart.test.ts create mode 100644 tsconfig.json create mode 100644 typings/@ms/odsp.d.ts create mode 100644 typings/tsd.d.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8ffcdc4ec --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index 00cbbdf53..d8f2cbaac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,58 +2,37 @@ logs *.log npm-debug.log* -yarn-debug.log* -yarn-error.log* -# Runtime data -pids -*.pid -*.seed -*.pid.lock +# Dependency directories +node_modules -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +# Build generated files +dist +lib +solution +temp +*.sppkg # Coverage directory used by tools like istanbul coverage -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +# OSX +.DS_Store -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj -# Optional eslint cache -.eslintcache +# Resx Generated Code +*.resx.ts -# Optional REPL history -.node_repl_history +# Styles Generated Code +*.scss.ts -# Output of 'npm pack' +# NPM packages *.tgz -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - +# VSCode +.vscode diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..dbe47024e --- /dev/null +++ b/.npmignore @@ -0,0 +1,22 @@ +# Folders +.vscode +coverage +node_modules +sharepoint +src +temp +config +typings +lib/webparts +assets +dist + +# Files +*.csproj +.git* +.yo-rc.json +.editorconfig +gulpfile.js +tsconfig.json +yarn.lock +*.tgz diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 000000000..eaa53a93b --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,8 @@ +{ + "@microsoft/generator-sharepoint": { + "version": "1.2.0", + "libraryName": "sp-dev-fx-controls-react", + "libraryId": "92b1e52c-a5fa-490a-bcf4-76080f39442c", + "environment": "spo" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 72f1506a9..639d7850b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# sp-dev-fx-controls-react + +React controls for the SharePoint Framework solutions. + +# work in progress # Contributing diff --git a/config/config.json b/config/config.json new file mode 100644 index 000000000..9eeff64bb --- /dev/null +++ b/config/config.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "controls-test-web-part": { + "components": [{ + "entrypoint": "./lib/webparts/controlsTest/ControlsTestWebPart.js", + "manifest": "./src/webparts/controlsTest/ControlsTestWebPart.manifest.json" + }] + } + }, + "externals": {}, + "localizedResources": { + "ControlStrings": "lib/loc/{locale}.js", + "ControlsTestWebPartStrings": "lib/webparts/controlsTest/loc/{locale}.js" + } +} diff --git a/config/copy-assets.json b/config/copy-assets.json new file mode 100644 index 000000000..e1bb26179 --- /dev/null +++ b/config/copy-assets.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json", + "deployCdnPath": "temp/deploy" +} diff --git a/config/deploy-azure-storage.json b/config/deploy-azure-storage.json new file mode 100644 index 000000000..2fc5f2fa6 --- /dev/null +++ b/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./temp/deploy/", + "account": "", + "container": "sp-dev-fx-controls-react", + "accessKey": "" +} \ No newline at end of file diff --git a/config/package-solution.json b/config/package-solution.json new file mode 100644 index 000000000..fd91c6eaf --- /dev/null +++ b/config/package-solution.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "sp-dev-fx-controls-react-client-side-solution", + "id": "92b1e52c-a5fa-490a-bcf4-76080f39442c", + "version": "1.0.0.0", + "skipFeatureDeployment": true + }, + "paths": { + "zippedPackage": "solution/sp-dev-fx-controls-react.sppkg" + } +} diff --git a/config/serve.json b/config/serve.json new file mode 100644 index 000000000..dbd4dbc10 --- /dev/null +++ b/config/serve.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json", + "port": 4321, + "initialPage": "https://localhost:5432/workbench", + "https": true, + "api": { + "port": 5432, + "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" + } +} diff --git a/config/tslint.json b/config/tslint.json new file mode 100644 index 000000000..0bb934c20 --- /dev/null +++ b/config/tslint.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json", + // Display errors as warnings + "displayAsWarning": true, + // The TSLint task may have been configured with several custom lint rules + // before this config file is read (for example lint rules from the tslint-microsoft-contrib + // project). If true, this flag will deactivate any of these rules. + "removeExistingRules": true, + // When true, the TSLint task is configured with some default TSLint "rules.": + "useDefaultConfigAsBase": false, + // Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules + // which are active, other than the list of rules below. + "lintConfig": { + // Opt-in to Lint rules which help to eliminate bugs in JavaScript + "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-case": true, + "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, + "valid-typeof": true, + "variable-name": false, + "whitespace": false + } + } +} \ No newline at end of file diff --git a/config/write-manifests.json b/config/write-manifests.json new file mode 100644 index 000000000..3506b9ea5 --- /dev/null +++ b/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 000000000..7d36ddb1c --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,6 @@ +'use strict'; + +const gulp = require('gulp'); +const build = require('@microsoft/sp-build-web'); + +build.initialize(gulp); diff --git a/package.json b/package.json new file mode 100644 index 000000000..04b937e5d --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "sp-dev-fx-controls-react", + "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.2.0", + "@microsoft/sp-webpart-base": "~1.2.0", + "@types/webpack-env": ">=1.12.1 <1.14.0", + "react": "15.4.2", + "react-dom": "15.4.2", + "@types/react": "15.0.38", + "@types/react-dom": "0.14.18", + "@types/react-addons-shallow-compare": "0.14.17", + "@types/react-addons-update": "0.14.14", + "@types/react-addons-test-utils": "0.14.15" + }, + "devDependencies": { + "@microsoft/sp-build-web": "~1.2.0", + "@microsoft/sp-module-interfaces": "~1.2.0", + "@microsoft/sp-webpart-workbench": "~1.2.0", + "gulp": "~3.9.1", + "@types/chai": ">=3.4.34 <3.6.0", + "@types/mocha": ">=2.2.33 <2.6.0" + } +} diff --git a/src/FileTypeIcon.ts b/src/FileTypeIcon.ts new file mode 100644 index 000000000..e5806660b --- /dev/null +++ b/src/FileTypeIcon.ts @@ -0,0 +1 @@ +export * from './controls/fileTypeIcon/index'; diff --git a/src/ListView.ts b/src/ListView.ts new file mode 100644 index 000000000..8664f4feb --- /dev/null +++ b/src/ListView.ts @@ -0,0 +1 @@ +export * from './controls/listView/index'; diff --git a/src/Placeholder.ts b/src/Placeholder.ts new file mode 100644 index 000000000..4534b97ec --- /dev/null +++ b/src/Placeholder.ts @@ -0,0 +1 @@ +export * from './controls/placeholder/index'; diff --git a/src/SiteBreadcrumb.ts b/src/SiteBreadcrumb.ts new file mode 100644 index 000000000..1716c08d8 --- /dev/null +++ b/src/SiteBreadcrumb.ts @@ -0,0 +1 @@ +export * from './controls/siteBreadcrumb/index'; diff --git a/src/controls/fileTypeIcon/FileTypeIcon.tsx b/src/controls/fileTypeIcon/FileTypeIcon.tsx new file mode 100644 index 000000000..5aeceab22 --- /dev/null +++ b/src/controls/fileTypeIcon/FileTypeIcon.tsx @@ -0,0 +1,222 @@ +import * as React from "react"; +import { findIndex } from '@microsoft/sp-lodash-subset'; +import { IFileTypeIconProps, ApplicationType, ApplicationIconList, IconType, IconSizes, ImageSize, IImageResult, ICON_GENERIC_16, ICON_GENERIC_48, ICON_GENERIC_96 } from "./IFileTypeIcon"; + +const ICON_GENERIC = "Page"; +const ICON_DEFAULT_SIZE = "icon16"; + +/** + * File type icon component + */ +export class FileTypeIcon extends React.Component { + constructor(props: IFileTypeIconProps) { + super(props); + } + + /** + * @function + * Function which returns the font icon + */ + private _getIconClassName(): string { + let className = ICON_GENERIC; + + // Check if the path property is provided + if (typeof this.props.path !== "undefined" && this.props.path !== null) { + const path: string = this.props.path; + const fileExtension: string = this._getFileExtension(path); + // Check the known file extensions list + const iconName = this._getIconByExtension(fileExtension.toLowerCase(), IconType.font); + if (iconName !== null) { + className = iconName; + } + } + // Check if the application name has been provided + else if (typeof this.props.application !== "undefined" && this.props.application !== null) { + const application: ApplicationType = this.props.application; + const iconName = this._getIconByApplicationType(application, IconType.font); + if (iconName !== null) { + className = iconName; + } + } + + return className; + } + + + /** + * @function + * Function which returns the image icon + */ + private _getIconImageName(): IImageResult { + let size = ICON_DEFAULT_SIZE; + let image: string | null = null; + + // Get the right icon size to display + if (typeof this.props.size !== "undefined" && this.props.size !== null) { + // Retrieve the right icon size + size = this._getFileSizeName(this.props.size); + } + + // Check if the path is provided + if (typeof this.props.path !== "undefined" && this.props.path !== null) { + const path: string = this.props.path; + const fileExtension: string = this._getFileExtension(path); + // Get the image for the current file extension + image = this._getIconByExtension(fileExtension.toLowerCase(), IconType.image); + } + // Check if the application name has been provided + else if (typeof this.props.application !== "undefined" && this.props.application !== null) { + const application: ApplicationType = this.props.application; + image = this._getIconByApplicationType(application, IconType.image); + } + + return { + size: size, + image: image + }; + } + + /** + * @function + * Function to retrieve the file extension from the path + * + * @param value File path + */ + private _getFileExtension(value): string { + // Split the URL on the dots + const splittedValue = value.split('.'); + // Take the last value + let extensionValue = splittedValue.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; + } + + /** + * @function + * Find the icon name for the provided extension + * + * @param extension File extension + */ + private _getIconByExtension(extension: string, iconType: IconType): string { + // Find the application index by the provided extension + const appIdx = findIndex(ApplicationIconList, item => { return item.extensions.indexOf(extension.toLowerCase()) !== -1; }); + + // Check if an application has found + if (appIdx !== -1) { + // Check the type of icon, the image needs to get checked for the name + if (iconType === IconType.font) { + return ApplicationIconList[appIdx].iconName; + } else { + const knownImgs = ApplicationIconList[appIdx].imageName; + // Check if the file extension is known + const imgIdx = knownImgs.indexOf(extension); + if (imgIdx !== -1) { + return knownImgs[imgIdx]; + } else { + // Return the first one if it was not known + return knownImgs[0]; + } + } + } + + return null; + } + + /** + * @function + * Find the icon name for the application + * + * @param application + */ + private _getIconByApplicationType(application: ApplicationType, iconType: IconType): string { + // Find the application index by the provided extension + const appIdx = findIndex(ApplicationIconList, item => item.application === application); + + // Check if an application has found + if (appIdx !== -1) { + const knownApp = ApplicationIconList[appIdx]; + if (iconType === IconType.font) { + return knownApp.iconName; + } else { + // Check if the application has a known list of image types + if (knownApp.imageName.length > 0) { + return knownApp.imageName[0]; + } + } + } + + return null; + } + + /** + * @function + * Return the right image size for the provided value + * + * @param value Image size value + */ + private _getFileSizeName(value: ImageSize): string { + // Find the image size index by the image size + const sizeIdx = findIndex(IconSizes, size => size.size === value); + + // Check if an icon size has been retrieved + if (sizeIdx !== -1) { + // Return the first icon size + return IconSizes[sizeIdx].name; + } + + // Return the default file size if nothing was found + return ICON_DEFAULT_SIZE; + } + + /** + * Default React component render method + */ + public render(): React.ReactElement { + let iconElm = ; + // Check the type of icon that needs to be displayed + if (this.props.type === IconType.image) { + // Return an image icon element + const iconImage = this._getIconImageName(); + // Check if the image was found, otherwise a generic image will be returned + if (typeof iconImage.image !== "undefined" && iconImage.image !== null) { + iconElm =
; + } else { + // Return a generic image + let imgElm = ; + // Check the size of the generic image which has to be returned + switch (iconImage.size) { + case "icon16": + imgElm = ; + break; + case "icon48": + imgElm = ; + break; + case "icon96": + imgElm = ; + break; + default: + imgElm = ; + break; + } + + iconElm = ( +
+ {imgElm} +
+ ); + } + } else { + // Return the icon font element + const iconClass = this._getIconClassName(); + iconElm = ; + } + + // Return the icon element + return iconElm; + } +} diff --git a/src/controls/fileTypeIcon/IFileTypeIcon.ts b/src/controls/fileTypeIcon/IFileTypeIcon.ts new file mode 100644 index 000000000..c20cf52f2 --- /dev/null +++ b/src/controls/fileTypeIcon/IFileTypeIcon.ts @@ -0,0 +1,213 @@ +/** + * Available icon types + */ +export enum IconType { + font, + image +} + +/** + * Available applications / types + */ +export enum ApplicationType { + Access = 0, + ASPX, + Code, + CSS, + CSV, + Excel, + HTML, + Image, + Mail, + OneNote, + PDF, + PowerApps, + PowerPoint, + Project, + Publisher, + SASS, + Visio, + Word +} + +/** + * @enum + * Available image sizes + */ +export enum ImageSize { + small, + medium, + large +} + +/** + * @interface + * Interface for the application icons list + */ +export interface IApplicationIcons { + application: ApplicationType; + extensions: string[]; + iconName: string; + imageName: string[]; +} + +/** + * Array with all the known applications and their icon and image names + */ +export const ApplicationIconList: IApplicationIcons[] = [ + { + application: ApplicationType.Access, + extensions: ["accdb", "accde", "accdt", "accdr", "mdb"], + iconName: "AccessLogo", + imageName: ["accdb"] + }, + { + application: ApplicationType.ASPX, + extensions: ["aspx", "master"], + iconName: "FileASPX", + imageName: [] + }, + { + application: ApplicationType.Code, + extensions: ["js", "ts", "cs"], + iconName: "FileCode", + imageName: [] + }, + { + application: ApplicationType.CSS, + extensions: ["css"], + iconName: "FileCSS", + imageName: [] + }, + { + application: ApplicationType.CSV, + extensions: ["csv"], + iconName: "ExcelDocument", + imageName: ["csv"] + }, + { + application: ApplicationType.Excel, + extensions: ["xls", "xlt", "xlm", "xlsx", "xlsm", "xltx", "xltm", "ods"], + iconName: "ExcelDocument", + imageName: ["xlsx", "xls", "xltx", "ods"] + }, + { + application: ApplicationType.HTML, + extensions: ["html"], + iconName: "FileHTML", + imageName: [] + }, + { + application: ApplicationType.Image, + extensions: ["jpg", "jpeg", "gif", "png"], + iconName: "FileImage", + imageName: [] + }, + { + application: ApplicationType.Mail, + extensions: ["msg"], + iconName: "Mail", + imageName: [] + }, + { + application: ApplicationType.OneNote, + extensions: ["one", "onepkg", "onetoc"], + iconName: "OneNoteLogo", + imageName: ["one", "onepkg", "onetoc"] + }, + { + application: ApplicationType.PDF, + extensions: ["pdf"], + iconName: "PDF", + imageName: [] + }, + { + application: ApplicationType.PowerApps, + extensions: ["msapp"], + iconName: "PowerApps", + imageName: [] + }, + { + application: ApplicationType.PowerPoint, + extensions: ["ppt", "pot", "pps", "pptx", "pptm", "potx", "potm", "ppam", "ppsx", "ppsm", "sldx", "sldx"], + iconName: "PowerPointDocument", + imageName: ["odp", "potx", "ppsx", "pptx"] + }, + { + application: ApplicationType.Project, + extensions: ["mpp", "mpt", "mpx", "mpd"], + iconName: "ProjectLogoInverse", + imageName: ["mpp", "mpt"] + }, + { + application: ApplicationType.Publisher, + extensions: ["pub"], + iconName: "PublisherLogo", + imageName: ["pub"] + }, + { + application: ApplicationType.SASS, + extensions: ["scss", "sass"], + iconName: "FileSass", + imageName: [] + }, + { + application: ApplicationType.Visio, + extensions: ["vsd", "vss", "vst", "vdx", "vsx", "vtx", "vsdx"], + iconName: "VisioDocument", + imageName: ["vsdx", "vssx", "vstx"] + }, + { + application: ApplicationType.Word, + extensions: ["doc", "dot", "docx", "docm", "dotx", "dotm", "docb", "odt"], + iconName: "WordDocument", + imageName: ["docx", "dotx", "odt"] + } +]; + +/** + * Array with the known icon image sizes + */ +export const IconSizes = [ + { + size: ImageSize.small, + name: "icon16" + }, + { + size: ImageSize.medium, + name: "icon48" + }, + { + size: ImageSize.large, + name: "icon96" + } +]; + +/** + * @interface + * Interface for the image result when return the image instead of the icon font + */ +export interface IImageResult { + size: string; + image: string; +} + +/** + * @interface + * Interface for the FileTypeIcon component properties + */ +export interface IFileTypeIconProps { + type: IconType; + application?: ApplicationType; + path?: string; + size?: ImageSize; +} + +/** + * Generic file type icons base64 encoded + */ +export const ICON_GENERIC_16 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAACxUExURf///6CgoZeYmJ+goJSVlpydnaampqWlpaOjo6KioqCgoZ+fn52enpycnJqbm/39/fz9/fz8/dzc3Pr7+/r6+/n6+vn5+vj5+vj4+erq6oyMjJGSk/n6+/f4+fv8/JCRkvj5+Y+QkPv7/I2Oj/f4+IyNjvb3+IqMjJmZmvz8/ImKi5eYmPf3+IeJipaXl/X294aHiJSVlvb394SGh5OUlPb294OFhZKTk4GDhICCg////6edRp4AAAAGdFJOUwD5MPww+XxQO+MAAAABYktHRACIBR1IAAAAsElEQVQY0z3L2RqCIBCGYdq0MkPCgqxxQaI9LFvs/m8smIP+s++dZwghQTieTKNZPO/1CS6klCaMsWAxGCKMKU+XKyGD9WKEMKGpBxFk2QZhStPl1gFAXiBEiW9ZSsgrhBlzZwluqkaImVgJKAG02iHMmRAuQRu1RzgcSyl9G3VCOOf+/2KM4VeEW661T2N5g3BXvszDWt4iPD28rBt/I3z4fx1CUdW7/enatO/uS8gP83oU3WB3gUcAAAAASUVORK5CYII="; + +export const ICON_GENERIC_48 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAGeUExURf///6Kio5iZmaSkpZeYmKOjpJaXl6ChopWWlp+goZSVlZ6en5OTlJucnZGSk5qbm6ampqWlpaSkpaSkpKOjo6KioqGioqGhoaCgoJ+goJ+fn56fn56enp2enp2dnZydnZycnJubnJqbm5qam5mamv39/fz9/fz8/fz9/Pz8/Pv8/P3+/unp6fv7+/r7+/r6+/r6+vn6+vn6+/n5+vj5+vj4+ff4+fj5+fLy8unq6vT09PHx8ejp6fv7/PPz8+/v7+Xl5e3t7evr6+Hh4e7u7ufn597e3vn6+erq6ujo6OPj49ra2vn5+YyMjJCSkuLi497f4N7f397e393e3+Hh4pCRkff4+Pf3+Pb3+I+QkY+QkI6PkI6Pj42Oj4yOjoyNjouNjYuMjYqMjIqLjImLi4mKi/b294iKipmZmvX294eJipiZmfb394eIiZiYmYaIiZeYmIaHiJeXmIWHiJaXl4WGh5WWl4SGh5WWloSFhpSVloOFhpSVlYKEhZOUlZOTlIKDhJKTlIGDhJKTk4CCg5GSkn+Bgn+BgX6Agf///0viRIgAAAAQdFJOUwD5MPww/DD8MPww/DD7MPl5g3acAAAAAWJLR0QAiAUdSAAAAmRJREFUSMfN0Wdf01AUwOG6twYolL3T2NjeEpMmao1AHTgq1kEb9i4Gi4iIiFhFrODH9o4kve1J+5rz+v+cc/NLIHByRxAaGpuCweaWUKi1rb2js6u7p7evf2Dw1OlaQHQmjEeSbkpSRJIjt4TombP+oEGMxRCKx+NDinJbVRVNS2i6IQt37p477wsaMWCCBxHhXvL+hYt+oEk0AdAweJAcHrl02QcE6ZOqL8hCcng09fDK1VqA+waVgUejqejjJ9euA9Assv34AAFjLkg9jT4bef7iBgAtIuvxAUVVVUNLM/CSzTgAITEeLx9wvtlIvMpkMq/fvH03UQMMsd59kZHQ8WRzljkJQKvIcvdAWsNA98AUAG1hheW41wxygLzIA9MAtIfd9ao2Rr+AO2CZMwB0hNl21qfZF5TBLACdkspGMWiPH+QcoGAOgC4GFLye9uRB3gHLnAegW9LwJ5BcTdDeexAFCwD0SBodlawv9+yAZS4C0Cux2llf2VvmEgB9kUSC1pW9C5YB6HeeRHO9qrdiKwAMEKCTmuRVvYVWARiMGGy8nOvzaA2A9wTopHZyvrfROgAf2Ee7Nd8XbBttAPBR1vnJsh9Gx8ZgE4BPckXO9XkCtgD4LPvntLfRNgBfZK4u5wXa22gHgK9ylk6Oz9l6AnYB+GbmcpUx19toD4DvplU9Ba+30T4AP8zaOQZFAH6atXNf8IsH+cocgwMAfpve7uqagEMA/sTydGzfQSUA/iK7zviA8frgCIAJVHeOAZicmp6ZnZtfWFxaXlldW9/Y3Nre2d3bLxYPDkulo+N/gZM7/wGRlGTTsm+SowAAAABJRU5ErkJggg=="; + +export const ICON_GENERIC_96 = "data:image/png;base64,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"; \ No newline at end of file diff --git a/src/controls/fileTypeIcon/index.ts b/src/controls/fileTypeIcon/index.ts new file mode 100644 index 000000000..82f3df8cb --- /dev/null +++ b/src/controls/fileTypeIcon/index.ts @@ -0,0 +1,2 @@ +export * from './IFileTypeIcon'; +export * from './FileTypeIcon'; \ No newline at end of file diff --git a/src/controls/listView/IListView.ts b/src/controls/listView/IListView.ts new file mode 100644 index 000000000..f850b3730 --- /dev/null +++ b/src/controls/listView/IListView.ts @@ -0,0 +1,72 @@ +import { Selection, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList'; +import { IColumn } from 'office-ui-fabric-react/lib/components/DetailsList'; + +export { SelectionMode }; + +export interface IListViewProps { + /** + * Specify the name of the file URL path which will be used to show the file icon. + */ + iconFieldName?: string; + /** + * The items to render. + */ + items: any[]; + /** + * The fields you want to view in your list view + */ + viewFields?: IViewField[]; + /** + * Boolean value to indicate if the component should render in compact mode. + * Set to false by default + */ + compact?: boolean; + /** + * Specify the item selection mode. + * By default this is set to none. + */ + selectionMode?: SelectionMode; + /** + * Selection event that passes the selected item(s) + */ + selection?: (items: any[]) => void; +} + +export interface IListViewState { + /** + * The items to render. + */ + items: any[]; + /** + * Given column defitions. + * If none are provided, default columns will be created based on the item's properties. + */ + columns?: IColumn[]; +} + +export interface IViewField { + /** + * Name of the field + */ + name: string; + /** + * Name of the field that will be used as the column title + */ + displayName?: string; + /** + * Specify the field name that needs to be used to render a link + */ + linkPropertyName?: string; + /** + * Specify if you want to enable column sorting + */ + sorting?: boolean; + /** + * Specify the maximum width of the column + */ + maxWidth?: number; + /** + * Override the render method of the field + */ + render?: (item?: any, index?: number, column?: IColumn) => any; +} \ No newline at end of file diff --git a/src/controls/listView/ListView.tsx b/src/controls/listView/ListView.tsx new file mode 100644 index 000000000..9a03491af --- /dev/null +++ b/src/controls/listView/ListView.tsx @@ -0,0 +1,233 @@ +import * as React from "react"; +import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode } from 'office-ui-fabric-react/lib/DetailsList'; +import { IListViewProps, IListViewState, IViewField } from "./IListView"; +import { IColumn } from "office-ui-fabric-react/lib/components/DetailsList"; +import { findIndex, has, sortBy, isEqual } from '@microsoft/sp-lodash-subset'; +import { FileTypeIcon, IconType } from "../fileTypeIcon/index"; + +/** + * File type icon component + */ +export class ListView extends React.Component { + private _selection: Selection; + + constructor(props: IListViewProps) { + super(props); + + // Initialize state + this.state = { + items: [] + }; + + // Binding the functions + this._columnClick = this._columnClick.bind(this); + + // Initialize the selection + this._selection = new Selection({ + // Create the event handler when a selection changes + onSelectionChanged: () => this.props.selection(this._selection.getSelection()) + }); + } + + /** + * Lifecycle hook to check if the component has to get updated + * @param nextProps + * @param nextState + */ + public shouldComponentUpdate?(nextProps: IListViewProps, nextState: IListViewState): boolean { + // Check if the new property set is updated or not + if (isEqual(this.props, nextProps) && isEqual(this.state, nextState)) { + return false; + } + return true; + } + + /** + * Lifecycle hook when new properties are retrieved + * @param nextProps + */ + public componentWillReceiveProps(nextProps: IListViewProps): void { + let tempState: IListViewState = this.state; + let columns: IColumn[] = []; + + // Check if a set of items was provided + if (typeof nextProps.items !== "undefined" && nextProps.items !== null) { + tempState.items = this._flattenItems(nextProps.items); + } + + // Check if an icon needs to be shown + if (typeof nextProps.iconFieldName !== "undefined" && nextProps.iconFieldName !== null) { + const iconColumn = this._createIconColumn(nextProps.iconFieldName); + columns.push(iconColumn); + } + + // Check if view fields were provided + if (typeof nextProps.viewFields !== "undefined" && nextProps.viewFields !== null) { + columns = this._createColumns(nextProps.viewFields, columns); + } + + // Add the columns to the temporary state + tempState.columns = columns; + // Update the current component state with the new values + this.setState(tempState); + } + + /** + * Flatten all objects in every item + * @param items + */ + private _flattenItems(items: any[]): any[] { + // Flatten items + const flattenItems = items.map(item => { + // Flatten all objects in the item + return this._flattenItem(item); + }); + return flattenItems; + } + + /** + * Flatten all object in the item + * @param item + */ + private _flattenItem(item: any): any { + let flatItem = {}; + for (let parentPropName in item) { + // Check if property already exists + if (!item.hasOwnProperty(parentPropName)) continue; + + // Check if the property is of type object + if ((typeof item[parentPropName]) === 'object') { + // Flatten every object + const flatObject = this._flattenItem(item[parentPropName]); + for (let childPropName in flatObject) { + if (!flatObject.hasOwnProperty(childPropName)) continue; + flatItem[`${parentPropName}.${childPropName}`] = flatObject[childPropName]; + } + } else { + flatItem[parentPropName] = item[parentPropName]; + } + } + return flatItem; + } + + /** + * Create an icon column rendering + * @param iconField + */ + private _createIconColumn(iconFieldName: string): IColumn { + return { + key: 'fileType', + name: 'File Type', + iconName: 'Page', + isIconOnly: true, + fieldName: 'fileType', + minWidth: 16, + maxWidth: 16, + onRender: (item: any) => { + return ( + + ); + } + }; + } + + /** + * Returns required set of columns for the list view + * @param viewFields + */ + private _createColumns(viewFields: IViewField[], crntColumns: IColumn[]): IColumn[] { + viewFields.forEach(field => { + crntColumns.push({ + key: field.name, + name: field.displayName || field.name, + fieldName: field.name, + minWidth: 0, + maxWidth: field.maxWidth, + onRender: this._fieldRender(field), + onColumnClick: this._columnClick + }); + }); + return crntColumns; + } + + /** + * Check how field needs to be rendered + * @param field + */ + private _fieldRender(field: IViewField): any | void { + // Check if a render function is specified + if (field.render) { + return field.render; + } + + // Check if the URL property is specified + if (field.linkPropertyName) { + return (item: any, index?: number, column?: IColumn) => { + return {item[column.fieldName]}; + }; + } + } + + /** + * Check if sorting needs to be set to the column + * @param ev + * @param column + */ + private _columnClick(ev: React.MouseEvent, column: IColumn): void { + // Find the field in the viewFields list + const columnIdx = findIndex(this.props.viewFields, field => field.name === column.key); + // Check if the field has been found + if (columnIdx !== -1) { + const field = this.props.viewFields[columnIdx]; + // Check if the field needs to be sorted + if (has(field, "sorting")) { + // Check if the sorting option is true + if (field.sorting) { + const sortedItems = this._sortItems(this.state.items, column.key, column.isSortedDescending); + // Update the columns + const sortedColumns = this.state.columns.map(c => { + if (c.key === column.key) { + c.isSortedDescending = !column.isSortedDescending; + c.isSorted = true; + } else { + c.isSorted = false; + c.isSortedDescending = false; + } + return c; + }); + // Update the items and columns + this.setState({ + items: sortedItems, + columns: sortedColumns + }); + } + } + } + } + + /** + * Sort the list of items by the clicked column + * @param items + * @param columnName + * @param descending + */ + private _sortItems(items: any[], columnName: string, descending = false): any[] { + const ascItems = sortBy(items, [columnName]); + return descending ? ascItems.reverse() : ascItems; + } + + /** + * Default React component render method + */ + public render(): React.ReactElement { + return ( + + ); + } +} diff --git a/src/controls/listView/index.ts b/src/controls/listView/index.ts new file mode 100644 index 000000000..0a038e2ee --- /dev/null +++ b/src/controls/listView/index.ts @@ -0,0 +1,2 @@ +export * from './IListView'; +export * from './ListView'; \ No newline at end of file diff --git a/src/controls/placeholder/IPlaceholderComponent.ts b/src/controls/placeholder/IPlaceholderComponent.ts new file mode 100644 index 000000000..482b2bff4 --- /dev/null +++ b/src/controls/placeholder/IPlaceholderComponent.ts @@ -0,0 +1,34 @@ +/** + * Used to display a placeholder in case of no or temporary content. Button is optional. + * + * @public + */ +export interface IPlaceholderProps { + /** + * Text description for the placeholder. Appears bellow the Icon and IconText. + */ + description: string; + /** + * Icon name used for the className from the MDL2 set. Example: 'Add'. + */ + iconName: string; + /** + * Heading displayed against the Icon. + */ + iconText: string; + /** + * Text label to be displayed on button below the description. + * Optional: As the button is optional. + */ + buttonLabel?: string; + /** + * onConfigure handler for the button. + * Optional: As the button is optional. + */ + onConfigure?: () => void; + /** + * This className is applied to the root element of content. Use this to + * apply custom styles to the placeholder. + */ + contentClassName?: string; +} \ No newline at end of file diff --git a/src/controls/placeholder/PlaceholderComponent.module.scss b/src/controls/placeholder/PlaceholderComponent.module.scss new file mode 100644 index 000000000..56d27aff5 --- /dev/null +++ b/src/controls/placeholder/PlaceholderComponent.module.scss @@ -0,0 +1,68 @@ +.placeholder { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + .placeholderContainer { + -webkit-box-align: center; + -ms-flex-align: center; + -ms-grid-row-align: center; + align-items: center; + color: "[theme:neutralSecondary, default: #666666]"; + background-color: "[theme:neutralLighter, default: #f4f4f4]"; + width: 100%; + padding: 80px 0; + .placeholderHead { + color: "[theme:neutralPrimary, default: #333333]"; + .placeholderHeadContainer { + height: 100%; + white-space: nowrap; + text-align: center; + } + .placeholderIcon { + display: inline-block; + vertical-align: middle; + white-space: normal; + } + .placeholderText { + display: inline-block; + vertical-align: middle; + white-space: normal + } + } + .placeholderDescription { + width: 65%; + vertical-align: middle; + margin: 0 auto; + text-align: center; + .placeholderDescriptionText { + color: "[theme:neutralSecondary, default: #666666]"; + font-size: 17px; + display: inline-block; + margin: 24px 0; + font-weight: 100; + } + } + } +} + +[dir=ltr] .placeholder, +[dir=rtl] .placeholder { + .placeholderContainer { + .placeholderHead { + .placeholderText { + padding-left: 20px; + } + } + } +} + +.placeholderOverlay { + position: relative; + height: 100%; + z-index: 1; + .placeholderSpinnerContainer { + position: relative; + width: 100%; + margin: 164px 0 + } +} \ No newline at end of file diff --git a/src/controls/placeholder/PlaceholderComponent.tsx b/src/controls/placeholder/PlaceholderComponent.tsx new file mode 100644 index 000000000..48c1f8350 --- /dev/null +++ b/src/controls/placeholder/PlaceholderComponent.tsx @@ -0,0 +1,61 @@ +import * as React from "react"; +import { IPlaceholderProps } from "./IPlaceholderComponent"; +import { Button, ButtonType } from 'office-ui-fabric-react/lib/components/Button'; +import styles from './PlaceholderComponent.module.scss'; + +/** + * Placeholder component + */ +export class Placeholder extends React.Component { + /** + * Constructor + */ + constructor(props: IPlaceholderProps) { + super(props); + + this._handleBtnClick = this._handleBtnClick.bind(this); + } + + /** + * Execute the onConfigure function + */ + private _handleBtnClick(event?: React.MouseEvent) { + this.props.onConfigure(); + } + + /** + * Default React component render method + */ + public render(): React.ReactElement { + const iconName = typeof this.props.iconName !== "undefined" && this.props.iconName !== null ? `ms-Icon--${this.props.iconName}` : ''; + + return ( +
+
+
+
+ + {this.props.iconText} +
+
+
+ {this.props.description} +
+ {this.props.children} +
+ { + this.props.buttonLabel && + + } +
+
+
+ ); + } +} \ No newline at end of file diff --git a/src/controls/placeholder/index.ts b/src/controls/placeholder/index.ts new file mode 100644 index 000000000..f848b9574 --- /dev/null +++ b/src/controls/placeholder/index.ts @@ -0,0 +1,2 @@ +export * from './IPlaceholderComponent'; +export * from './PlaceholderComponent'; \ No newline at end of file diff --git a/src/controls/siteBreadcrumb/ISiteBreadcrumb.ts b/src/controls/siteBreadcrumb/ISiteBreadcrumb.ts new file mode 100644 index 000000000..1161e2d44 --- /dev/null +++ b/src/controls/siteBreadcrumb/ISiteBreadcrumb.ts @@ -0,0 +1,18 @@ +import ApplicationCustomizerContext from "@microsoft/sp-application-base/lib/extensibility/ApplicationCustomizerContext"; +import { IBreadcrumbItem } from "office-ui-fabric-react/lib/Breadcrumb"; +import { WebPartContext } from "@microsoft/sp-webpart-base"; + +export interface ISiteBreadcrumbProps { + context: WebPartContext | ApplicationCustomizerContext; +} + +export interface ISiteBreadcrumbState { + breadcrumbItems: IBreadcrumbItem[]; +} + +export interface IWebInfo { + Id: string; + Title: string; + ServerRelativeUrl: string; + error?: any; +} diff --git a/src/controls/siteBreadcrumb/SiteBreadcrumb.module.scss b/src/controls/siteBreadcrumb/SiteBreadcrumb.module.scss new file mode 100644 index 000000000..4fc1c2781 --- /dev/null +++ b/src/controls/siteBreadcrumb/SiteBreadcrumb.module.scss @@ -0,0 +1,25 @@ +.breadcrumb { + >div { + padding-left: 7px; + } + div[role="navigation"] { + margin-top: 0; + } + .breadcrumbLinks { + ol { + li { + a { + font-size: 14px; + padding: 4px 8px 8px 8px; + &:hover { + background-color: #fff; + } + } + i { + font-size: 10px; + margin: 10px 0; + } + } + } + } +} diff --git a/src/controls/siteBreadcrumb/SiteBreadcrumb.tsx b/src/controls/siteBreadcrumb/SiteBreadcrumb.tsx new file mode 100644 index 000000000..54f2a3186 --- /dev/null +++ b/src/controls/siteBreadcrumb/SiteBreadcrumb.tsx @@ -0,0 +1,129 @@ +import * as React from "react"; +import { ISiteBreadcrumbProps, ISiteBreadcrumbState, IWebInfo } from "./ISiteBreadcrumb"; +import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcrumb'; +import { SPHttpClient, HttpClientResponse } from "@microsoft/sp-http"; +import styles from './SiteBreadcrumb.module.scss'; +import * as strings from 'ControlStrings'; + +/** + * Site breadcrumb component + */ +export class SiteBreadcrumb extends React.Component { + private _linkItems: IBreadcrumbItem[]; + + constructor(props: ISiteBreadcrumbProps) { + super(props); + + // Initiate the private link items variable + this._linkItems = []; + + // Initiate the component state + this.state = { + breadcrumbItems: [] + }; + } + + /** + * React component lifecycle hook, runs after render + */ + public componentDidMount() { + // Start generating the links for the breadcrumb + this._generateLinks(); + } + + /** + * Start the link generation for the breadcrumb + */ + private _generateLinks() { + // Add the current site to the links list + this._linkItems.push({ + text: this.props.context.pageContext.web.title, + key: this.props.context.pageContext.web.id.toString(), + href: this.props.context.pageContext.web.absoluteUrl, + isCurrentItem: !this.props.context.pageContext.list.serverRelativeUrl + }); + + // Check if the current list URL is available + if (!!this.props.context.pageContext.list.serverRelativeUrl) { + // Add the current list to the links list + this._linkItems.push({ + text: this.props.context.pageContext.list.title, + key: this.props.context.pageContext.list.id.toString(), + href: this.props.context.pageContext.list.serverRelativeUrl, + isCurrentItem: true + }); + } + + // Check if you are already on the root site + if (this.props.context.pageContext.site.serverRelativeUrl === this.props.context.pageContext.web.serverRelativeUrl) { + this._setBreadcrumbData(); + } else { + // Retrieve the parent webs information + this._getParentWeb(this.props.context.pageContext.web.absoluteUrl); + } + } + + /** + * Retrieve the parent web URLs + * @param webUrl Current URL of the web to process + */ + private _getParentWeb(webUrl: string) { + // Retrieve the parent web info + const apiUrl = `${webUrl}/_api/web/parentweb?$select=Id,Title,ServerRelativeUrl`; + this.props.context.spHttpClient.get(apiUrl, SPHttpClient.configurations.v1) + .then((response: HttpClientResponse) => { + return response.json(); + }) + .then((webInfo: IWebInfo) => { + if (!webInfo.error) { + // Check if the correct data is retrieved + if (!webInfo.ServerRelativeUrl && !webInfo.Title) { + this._setBreadcrumbData(); + return; + } + + // Store the current site + this._linkItems.unshift({ + text: webInfo.Title, + key: webInfo.Id, + href: webInfo.ServerRelativeUrl + }); + + // Check if you retrieved all the information up until the root site + if (webInfo.ServerRelativeUrl === this.props.context.pageContext.site.serverRelativeUrl) { + this._setBreadcrumbData(); + } else { + // retrieve the information from the parent site + webUrl = webUrl.substring(0, (webUrl.indexOf(`${webInfo.ServerRelativeUrl}/`) + webInfo.ServerRelativeUrl.length)); + this._getParentWeb(webUrl); + } + } else { + // Set the current breadcrumb data which is already retrieved + this._setBreadcrumbData(); + } + }); + } + + /** + * Set the current breadcrumb data + */ + private _setBreadcrumbData() { + this.setState({ + breadcrumbItems: this._linkItems + }); + } + + /** + * Default React component render method + */ + public render(): React.ReactElement { + return ( +
+ +
+ ); + } +} diff --git a/src/controls/siteBreadcrumb/index.ts b/src/controls/siteBreadcrumb/index.ts new file mode 100644 index 000000000..2c5e82f7d --- /dev/null +++ b/src/controls/siteBreadcrumb/index.ts @@ -0,0 +1,2 @@ +export * from './ISiteBreadcrumb'; +export * from './SiteBreadcrumb'; diff --git a/src/loc/en-us.js b/src/loc/en-us.js new file mode 100644 index 000000000..caabcc1d0 --- /dev/null +++ b/src/loc/en-us.js @@ -0,0 +1,5 @@ +define([], function () { + return { + "SiteBreadcrumbLabel": "Website breadcrumb" + } +}); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts new file mode 100644 index 000000000..92645c5b6 --- /dev/null +++ b/src/loc/mystrings.d.ts @@ -0,0 +1,8 @@ +declare interface IControlStrings { + SiteBreadcrumbLabel: string; +} + +declare module 'ControlStrings' { + const strings: IControlStrings; + export = strings; +} diff --git a/src/webparts/controlsTest/ControlsTestWebPart.manifest.json b/src/webparts/controlsTest/ControlsTestWebPart.manifest.json new file mode 100644 index 000000000..c8f39c97a --- /dev/null +++ b/src/webparts/controlsTest/ControlsTestWebPart.manifest.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "45165954-80f9-44c1-9967-cd38ae92a33b", + "alias": "ControlsTestWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + + "preconfiguredEntries": [{ + "groupId": "45165954-80f9-44c1-9967-cd38ae92a33b", + "group": { + "default": "Under Development" + }, + "title": { + "default": "ControlsTest" + }, + "description": { + "default": "ControlsTest description" + }, + "officeFabricIconFontName": "Settings", + "properties": { + "description": "ControlsTest" + } + }] +} diff --git a/src/webparts/controlsTest/ControlsTestWebPart.ts b/src/webparts/controlsTest/ControlsTestWebPart.ts new file mode 100644 index 000000000..7a53565b8 --- /dev/null +++ b/src/webparts/controlsTest/ControlsTestWebPart.ts @@ -0,0 +1,54 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { Version } from '@microsoft/sp-core-library'; +import { + BaseClientSideWebPart, + IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-webpart-base'; + +import * as strings from 'ControlsTestWebPartStrings'; +import ControlsTest from './components/ControlsTest'; +import { IControlsTestProps } from './components/IControlsTestProps'; +import { IControlsTestWebPartProps } from './IControlsTestWebPartProps'; + +export default class ControlsTestWebPart extends BaseClientSideWebPart { + + public render(): void { + const element: React.ReactElement = React.createElement( + ControlsTest, + { + context: this.context, + description: this.properties.description + } + ); + + ReactDom.render(element, this.domElement); + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('description', { + label: strings.DescriptionFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/webparts/controlsTest/IControlsTestWebPartProps.ts b/src/webparts/controlsTest/IControlsTestWebPartProps.ts new file mode 100644 index 000000000..46e07f3d7 --- /dev/null +++ b/src/webparts/controlsTest/IControlsTestWebPartProps.ts @@ -0,0 +1,3 @@ +export interface IControlsTestWebPartProps { + description: string; +} diff --git a/src/webparts/controlsTest/components/ControlsTest.module.scss b/src/webparts/controlsTest/components/ControlsTest.module.scss new file mode 100644 index 000000000..58bbcceeb --- /dev/null +++ b/src/webparts/controlsTest/components/ControlsTest.module.scss @@ -0,0 +1,44 @@ +.controlsTest { + .container { + max-width: 700px; + margin: 0px auto 20px auto; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + } + .row { + padding: 20px; + } + .listItem { + max-width: 715px; + margin: 5px auto 5px auto; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); + } + .button { + // Our button + text-decoration: none; + height: 32px; // Primary Button + min-width: 80px; + background-color: #0078d7; + border-color: #0078d7; + color: #ffffff; // Basic Button + outline: transparent; + position: relative; + font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + font-size: 14px; + font-weight: 400; + border-width: 0; + text-align: center; + cursor: pointer; + display: inline-block; + padding: 0 16px; + .label { + font-weight: 600; + font-size: 14px; + height: 32px; + line-height: 32px; + margin: 0 4px; + vertical-align: top; + display: inline-block; + } + } +} diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx new file mode 100644 index 000000000..72013609f --- /dev/null +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -0,0 +1,163 @@ +import * as React from 'react'; +import styles from './ControlsTest.module.scss'; +import { IControlsTestProps, IControlsTestState } from './IControlsTestProps'; +import { escape } from '@microsoft/sp-lodash-subset'; +import { FileTypeIcon, IconType, ApplicationType, ImageSize } from '../../../FileTypeIcon'; +import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/components/Dropdown"; +import { Placeholder } from '../../../Placeholder'; +import { ListView, IViewField, SelectionMode } from '../../../ListView'; +import { SPHttpClient } from '@microsoft/sp-http'; + +export default class ControlsTest extends React.Component { + constructor(props: IControlsTestProps) { + super(props); + + this.state = { + imgSize: ImageSize.small, + items: [] + }; + + this._onIconSizeChange = this._onIconSizeChange.bind(this); + this._onConfigure = this._onConfigure.bind(this); + } + + /** + * React componentDidMount lifecycle hook + */ + public componentDidMount() { + const restApi = `${this.props.context.pageContext.web.absoluteUrl}/_api/web/GetFolderByServerRelativeUrl('Shared%20Documents')/files?$expand=ListItemAllFields`; + this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1) + .then(resp => { return resp.json(); }) + .then(items => { + this.setState({ + items: items.value ? items.value : [] + }); + }); + } + + /** + * Event handler when changing the icon size in the dropdown + * @param element + */ + private _onIconSizeChange(element?: IDropdownOption): void { + this.setState({ + imgSize: parseInt(element.key.toString()) + }); + } + + /** + * Open the property pane + */ + private _onConfigure() { + this.props.context.propertyPane.open(); + } + + /** + * Method that retrieves the selected items in the list view + * @param items + */ + private _getSelection(items: any[]) { + console.log('Items:', items); + } + + /** + * Renders the component + */ + public render(): React.ReactElement { + // Size options for the icon size dropdown + const sizeOptions: IDropdownOption[] = [ + { + key: ImageSize.small, + text: ImageSize[ImageSize.small], + selected: ImageSize.small === this.state.imgSize + }, + { + key: ImageSize.medium, + text: ImageSize[ImageSize.medium], + selected: ImageSize.medium === this.state.imgSize + }, + { + key: ImageSize.large, + text: ImageSize[ImageSize.large], + selected: ImageSize.large === this.state.imgSize + } + ]; + + // Specify the fields that need to be viewed in the listview + const viewFields: IViewField[] = [ + { + name: "ListItemAllFields.Id", + displayName: "ID", + maxWidth: 20, + sorting: true + }, + { + name: "Name", + linkPropertyName: "ServerRelativeUrl", + sorting: true + }, + { + name: "ServerRelativeUrl", + displayName: "Path", + render: (item: any) => { + return Link; + } + }, + { + name: "Title" + } + ]; + + return ( +
+
+
+
+ Controls testing + +

+ File type icon control +

+

+ Font icons:  +   +   +   +   +   + +

+

+ Image icons:  +   +   +   +   +

+

Icon size tester: + + + +

+
+
+
+ + + + +
+ ); + } +} diff --git a/src/webparts/controlsTest/components/IControlsTestProps.ts b/src/webparts/controlsTest/components/IControlsTestProps.ts new file mode 100644 index 000000000..40c2cd366 --- /dev/null +++ b/src/webparts/controlsTest/components/IControlsTestProps.ts @@ -0,0 +1,12 @@ +import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { ImageSize } from "../../../FileTypeIcon"; + +export interface IControlsTestProps { + context: WebPartContext; + description: string; +} + +export interface IControlsTestState { + imgSize: ImageSize; + items: any[]; +} diff --git a/src/webparts/controlsTest/loc/en-us.js b/src/webparts/controlsTest/loc/en-us.js new file mode 100644 index 000000000..89f98bc1e --- /dev/null +++ b/src/webparts/controlsTest/loc/en-us.js @@ -0,0 +1,7 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Group Name", + "DescriptionFieldLabel": "Description Field" + } +}); \ No newline at end of file diff --git a/src/webparts/controlsTest/loc/mystrings.d.ts b/src/webparts/controlsTest/loc/mystrings.d.ts new file mode 100644 index 000000000..631c8431b --- /dev/null +++ b/src/webparts/controlsTest/loc/mystrings.d.ts @@ -0,0 +1,10 @@ +declare interface IControlsTestWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + DescriptionFieldLabel: string; +} + +declare module 'ControlsTestWebPartStrings' { + const strings: IControlsTestWebPartStrings; + export = strings; +} diff --git a/src/webparts/controlsTest/test/ControlsTestWebPart.test.ts b/src/webparts/controlsTest/test/ControlsTestWebPart.test.ts new file mode 100644 index 000000000..216e9dbde --- /dev/null +++ b/src/webparts/controlsTest/test/ControlsTestWebPart.test.ts @@ -0,0 +1,9 @@ +/// + +import { assert } from 'chai'; + +describe('ControlsTestWebPart', () => { + it('should do something', () => { + assert.ok(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..20a531bae --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "types": [ + "es6-promise", + "es6-collections", + "webpack-env" + ] + } +} diff --git a/typings/@ms/odsp.d.ts b/typings/@ms/odsp.d.ts new file mode 100644 index 000000000..5a2404000 --- /dev/null +++ b/typings/@ms/odsp.d.ts @@ -0,0 +1,11 @@ +// Type definitions for Microsoft ODSP projects +// Project: ODSP + +/* Global definition for UNIT_TEST builds + Code that is wrapped inside an if(UNIT_TEST) {...} + block will not be included in the final bundle when the + --ship flag is specified */ +declare const UNIT_TEST: boolean; + +/* Global defintion for SPO builds */ +declare const DATACENTER: boolean; \ No newline at end of file diff --git a/typings/tsd.d.ts b/typings/tsd.d.ts new file mode 100644 index 000000000..e7efdd728 --- /dev/null +++ b/typings/tsd.d.ts @@ -0,0 +1 @@ +///