New sample react-list-form (#373)

* Initial commit

* Create LICENSE

* readme updated

* Major extensions (more web part properties, field ordering, adding, refactoring list form rendering...)

* Updated to newest version of packages / support display field User & Lookup

* Refactored rendering of SPFormField / support more field types / Check fro local environment and notify user / Added ConfigureWebPart / Introduced showSupportedFields option

* Removed unused imports

* Use of resource strings for localization / ListForm component now not relying on web part context (just spHttpClient) / added comments / fixed bug where old values were still shown after switching to other form type or item / Moved date picer strings to localized resources

* Fixed bug where fields would not clear when switching formtype or id

* Updated Readme and added overview clip gif
This commit is contained in:
Dany 2017-12-01 15:45:08 +01:00 committed by Vesa Juvonen
parent 9c9807c4fd
commit 137526e5d4
72 changed files with 16572 additions and 0 deletions

View File

@ -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

32
samples/react-list-form/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,36 @@
{
"version": "0.2.0",
"configurations": [{
"name": "Local workbench",
"type": "chrome",
"request": "launch",
"url": "https://localhost:4321/temp/workbench.html",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
},
{
"name": "Hosted workbench",
"type": "chrome",
"request": "launch",
"url": "https://skybow.sharepoint.com/sites/test_dwy/_layouts/15/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
}
]
}

View File

@ -0,0 +1,75 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib",
"json.schemas": [
{
"fileMatch": [
"/config/config.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configJson/schemas/config-v1.schema.json"
},
{
"fileMatch": [
"/config/copy-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyAssets/copy-assets.schema.json"
},
{
"fileMatch": [
"/config/deploy-azure-storage.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/deployAzureStorage/deploy-azure-storage.schema.json"
},
{
"fileMatch": [
"/config/package-solution.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/packageSolution/package-solution.schema.json"
},
{
"fileMatch": [
"/config/serve.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-serve/lib/serve.schema.json"
},
{
"fileMatch": [
"/config/tslint.json"
],
"url": "./node_modules/@microsoft/gulp-core-build-typescript/lib/schemas/tslint.schema.json"
},
{
"fileMatch": [
"/config/write-manifests.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/writeManifests/write-manifests.schema.json"
},
{
"fileMatch": [
"/config/configure-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack.schema.json"
},
{
"fileMatch": [
"/config/configure-external-bundling-webpack.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/configureWebpack/configure-webpack-external-bundling.schema.json"
},
{
"fileMatch": [
"/copy-static-assets.json"
],
"url": "./node_modules/@microsoft/sp-build-core-tasks/lib/copyStaticAssets/copy-static-assets.schema.json"
}
]
}

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.2.0",
"libraryName": "react-form-webpart",
"libraryId": "373a20ef-dfc6-456a-95ec-171de3c94581",
"environment": "spo"
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Dany Wyss (Ardevia Software GmbH)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,53 @@
# React List Form WebPart
## Summary
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.
The web part allows configuring which list to use and if a form for adding a new item, editing or displaying an existing item should be shown. When selecting display or edit form the ID can be defined either as a fixed number or as a query string parameter name. The form fields can be added, ordered using drag-and-drop or removed visually in the web part. A URL including placeholder for the ID can be provided to redirect to after successfully saving the form.
![Demo](./assets/React-ListForm-Overview.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-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-list-form|Dany Wyss
## Version history
Version|Date|Comments
-------|----|--------
1.0|November 24, 2017|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 install`
- `gulp serve`
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Using React for building SharePoint Framework client-side web parts.
- Using React controlled components for SharePoint form fields.
- Using SharePoint REST services to retrieve and update schema and data for lists and fields.
- Using Office UI Fabric React components and styles for building user experience consistent with SharePoint and Office.
- Integrating drag and drop to provide better user experience for configuring web parts visually.
- Using custom drop down property editors in the property pane.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-list-form" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

View File

@ -0,0 +1,21 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"list-form-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/listForm/ListFormWebPart.js",
"manifest": "./src/webparts/listForm/ListFormWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ListFormWebPartStrings": "lib/webparts/listForm/loc/{locale}.js",
"ListFormStrings": "lib/webparts/listForm/components/loc/{locale}.js",
"FormFieldStrings": "lib/webparts/listForm/components/formFields/loc/{locale}.js",
"servicesStrings": "lib/common/services/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-form-webpart",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-form-webpart-client-side-solution",
"id": "373a20ef-dfc6-456a-95ec-171de3c94581",
"version": "1.0.0.0",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-form-webpart.sppkg"
}
}

View File

@ -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/"
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

6
samples/react-list-form/gulpfile.js vendored Normal file
View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

13584
samples/react-list-form/npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "react-form-webpart",
"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.3.4",
"@microsoft/sp-office-ui-fabric-core": "^1.3.4",
"@microsoft/sp-webpart-base": "~1.3.4",
"@types/react": "15.0.38",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dnd": "^2.0.34",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"moment": "^2.19.1",
"react": "15.4.2",
"react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4",
"react-dom": "15.4.2",
"spfx-uifabric-themes": "^0.1.3"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.4",
"@microsoft/sp-module-interfaces": "~1.3.4",
"@microsoft/sp-webpart-workbench": "~1.3.4",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,53 @@
export const Locales = {
1025: 'ar-SA',
1026: 'bg-BG',
1027: 'ca-ES',
1028: 'zh-TW',
1029: 'cs-CZ',
1030: 'da-DK',
1031: 'de-DE',
1032: 'el-GR',
1033: 'en-US',
1035: 'fi-FI',
1036: 'fr-FR',
1037: 'he-IL',
1038: 'hu-HU',
1040: 'it-IT',
1041: 'ja-JP',
1042: 'ko-KR',
1043: 'nl-NL',
1044: 'nb-NO',
1045: 'pl-PL',
1046: 'pt-BR',
1048: 'ro-RO',
1049: 'ru-RU',
1050: 'hr-HR',
1051: 'sk-SK',
1053: 'sv-SE',
1054: 'th-TH',
1055: 'tr-TR',
1057: 'id-ID',
1058: 'uk-UA',
1060: 'sl-SI',
1061: 'et-EE',
1062: 'lv-LV',
1063: 'lt-LT',
1066: 'vi-VN',
1068: 'az-Latn-AZ',
1069: 'eu-ES',
1071: 'mk-MK',
1081: 'hi-IN',
1086: 'ms-MY',
1087: 'kk-KZ',
1106: 'cy-GB',
1110: 'gl-ES',
1164: 'prs-AF',
2052: 'zh-CN',
2070: 'pt-PT',
2074: 'sr-Latn-CS',
2108: 'ga-IE',
3082: 'es-ES',
5146: 'bs-Latn-BA',
9242: 'sr-Latn-RS',
10266: 'sr-Cyrl-RS',
};

View File

@ -0,0 +1,166 @@
@import '~office-ui-fabric-core/dist/sass/References';
@import './node_modules/spfx-uifabric-themes/office.theme.vars';
/* themed tokens */
$ms-color-themeDarker: "[theme:themeDarker, default: #004578]";
$ms-color-themeDark: "[theme:themeDark, default: #005a9e]";
$ms-color-themeDarkAlt: "[theme:themeDarkAlt, default: #106ebe]";
$ms-color-themePrimary: "[theme:themePrimary, default: #0078d7]";
$ms-color-themeSecondary: "[theme:themeSecondary, default: #2b88d8]";
$ms-color-themeTertiary: "[theme:themeTertiary, default: #71afe5]";
$ms-color-themeLight: "[theme:themeLight, default: #c7e0f4]";
$ms-color-themeLighter: "[theme:themeLighter, default: #deecf9]";
$ms-color-themeLighterAlt: "[theme:themeLighterAlt, default: #eff6fc]";
$ms-color-black: "[theme:black, default: #000000]";
$ms-color-neutralDark: "[theme:neutralDark, default: #212121]";
$ms-color-neutralPrimary: "[theme:neutralPrimary, default: #333333]";
$ms-color-neutralPrimaryAlt: "[theme:neutralPrimaryAlt, default: #3c3c3c]";
$ms-color-neutralSecondary: "[theme:neutralSecondary, default: #666666]";
$ms-color-neutralSecondaryAlt: "[theme:neutralSecondaryAlt, default: #767676]";
$ms-color-neutralTertiary: "[theme:neutralTertiary, default: #a6a6a6]";
$ms-color-neutralTertiaryAlt: "[theme:neutralTertiaryAlt, default: #c8c8c8]";
$ms-color-neutralQuaternary: "[theme:neutralQuaternary, default: #d0d0d0]";
$ms-color-neutralQuaternaryAlt: "[theme:neutralQuaternaryAlt, default: #dadada]";
$ms-color-neutralLight: "[theme:neutralLight, default: #eaeaea]";
$ms-color-neutralLighter: "[theme:neutralLighter, default: #f4f4f4]";
$ms-color-neutralLighterAlt: "[theme:neutralLighterAlt, default: #f8f8f8]";
$ms-color-white: "[theme:white, default: #ffffff]";
$ms-color-blackTranslucent40: "[theme:blackTranslucent40, default: rgba(0,0,0,.4)]";
$ms-color-whiteTranslucent40: "[theme:whiteTranslucent40, default: rgba(255,255,255,.4)]";
$ms-color-yellow: "[theme:yellow, default: #ffb900]";
$ms-color-yellowLight: "[theme:yellowLight, default: #fff100]";
$ms-color-orange: "[theme:orange, default: #d83b01]";
$ms-color-orangeLight: "[theme:orangeLight, default: #ff8c00]";
$ms-color-redDark: "[theme:redDark, default: #a80000]";
$ms-color-red: "[theme:red, default: #e81123]";
$ms-color-magentaDark: "[theme:magentaDark, default: #5c005c]";
$ms-color-magenta: "[theme:magenta, default: #b4009e]";
$ms-color-magentaLight: "[theme:magentaLight, default: #e3008c]";
$ms-color-purpleDark: "[theme:purpleDark, default: #32145a]";
$ms-color-purple: "[theme:purple, default: #5c2d91]";
$ms-color-purpleLight: "[theme:purpleLight, default: #b4a0ff]";
$ms-color-blueDark: "[theme:blueDark, default: #002050]";
$ms-color-blueMid: "[theme:blueMid, default: #00188f]";
$ms-color-blue: "[theme:blue, default: #0078d7]";
$ms-color-blueLight: "[theme:blueLight, default: #00bcf2]";
$ms-color-tealDark: "[theme:tealDark, default: #004b50]";
$ms-color-teal: "[theme:teal, default: #008272]";
$ms-color-tealLight: "[theme:tealLight, default: #00b294]";
$ms-color-greenDark: "[theme:greenDark, default: #004b1c]";
$ms-color-green: "[theme:green, default: #107c10]";
$ms-color-greenLight: "[theme:greenLight, default: #bad80a]";
$ms-color-error: "[theme:error, default: #a80000]";
$ms-color-errorText: "[theme:errorText, default: #333333]";
$ms-color-errorBackground: "[theme:errorBackground, default: #fde7e9]";
$ms-color-success: "[theme:success, default: #107c10]";
$ms-color-successText: "[theme:successText, default: #333333]";
$ms-color-successBackground: "[theme:successBackground, default: #dff6dd]";
$ms-color-alert: "[theme:alert, default: #d83b01]";
$ms-color-alertText: "[theme:alertText, default: #333333]";
$ms-color-alertBackground: "[theme:alertBackground, default: #deecf9]";
$ms-color-warning: "[theme:warning, default: #767676]";
$ms-color-warningText: "[theme:warningText, default: #333333]";
$ms-color-warningBackground: "[theme:warningBackground, default: #fff4ce]";
$ms-color-severeWarning: "[theme:severeWarning, default: #d83b01]";
$ms-color-severeWarningText: "[theme:severeWarningText, default: #333333]";
$ms-color-severeWarningBackground: "[theme:severeWarningBackground, default: #fed9cc]";
$ms-color-info: "[theme:info, default: #767676]";
$ms-color-infoText: "[theme:infoText, default: #333333]";
$ms-color-infoBackground: "[theme:infoBackground, default: #f4f4f4]";
$ms-color-orangeLighter: "[theme:orangeLighter, default: #ea4300]";
/** Semantic Color Variables - taken from
* https://github.com/OfficeDev/office-ui-fabric-react/blob/master/packages/office-ui-fabric-react/src/common/_semanticColorVariables.scss
*/
$focusedBorderColor: $ms-color-neutralSecondary;
$bodyBackgroundColor: $ms-color-white;
$bodyForegroundColor: $ms-color-neutralPrimary;
/* Primary item colors, used for Nav, DetailsList headers, Pivots */
$unselectedBackgroundColor: $bodyBackgroundColor;
$unselectedForegroundColor: $bodyForegroundColor;
$disabledBackgroundColor: $bodyBackgroundColor;
$disabledForegroundColor: $ms-color-neutralTertiaryAlt;
$unselectedHoverBackgroundColor: $ms-color-neutralLighterAlt;
$unselectedHoverForegroundColor: $bodyForegroundColor;
$unselectedActiveBackgroundColor: $ms-color-neutralLight;
$unselectedActiveForegroundColor: $bodyForegroundColor;
$selectedBackgroundColor: $ms-color-neutralLighter;
$selectedForegroundColor: $ms-color-neutralPrimary;
$selectedHoverBackgroundColor: $ms-color-neutralLighter;
$selectedHoverForegroundColor: $ms-color-neutralDark;
$selectedActiveBackgroundColor: $ms-color-neutralTertiaryAlt;
$selectedActiveForegroundColor: $ms-color-neutralPrimary;
$dividerColor: $ms-color-neutralLight;
// *** New semantic slots
// todo: convert to theme tokens
/*** BASIC ***/
$bodyBackgroundColor: $ms-color-white;
$bodyTextColor: $ms-color-neutralPrimary;
$bodySubtextColor: $ms-color-neutralSecondary;
$disabledBackgroundColor: $ms-color-neutralLighter;
$disabledTextColor: $ms-color-neutralTertiaryAlt;
$disabledSubtextColor: $ms-color-neutralQuaternary;
$focusBorderColor: $ms-color-black;
// $errorBackgroundColor: todo
$errorTextColor: $ms-color-redDark;
/*** Text fields ***/
$fieldBackgroundColor: $ms-color-white;
$fieldTextColor: $ms-color-neutralPrimary;
$fieldBorderColor: $ms-color-neutralTertiary;
$fieldBorderHoverColor: $ms-color-neutralDark;
$fieldBorderFocusColor: $ms-color-themePrimary;
$fieldPlaceholderColor: $ms-color-neutralTertiary;
/*** State controls ***/
$controlBackgroundColor: $ms-color-white;
$controlBackgroundSelectedColor: $ms-color-themePrimary;
$controlBackgroundSelectedHoverColor: $ms-color-themeDarkAlt;
$controlForegroundColor: $ms-color-neutralPrimary;
$controlForegroundHoverColor: $ms-color-black;
$controlForegroundSelectedColor: $ms-color-white;
$controlBorderUnselectedColor: $ms-color-neutralTertiary;
$controlBorderUnselectedHoverColor: $ms-color-neutralPrimary;
// $controlBorderSelectedColor: unused
$controlBorderSelectedHoverColor: $ms-color-themePrimary;
/*** Menus ***/
$menuBackgroundColor: $ms-color-white;
$menuBackgroundHoverColor: $ms-color-neutralLighter;
$menuBackgroundSelectedColor: $ms-color-neutralQuaternaryAlt;
$menuTextColor: $ms-color-neutralPrimary;
$menuTextSelectedColor: $ms-color-black;
$menuTextDisabledColor: $ms-color-neutralTertiaryAlt;
$menuIconColor: $ms-color-themePrimary;
$menuHeaderColor: $ms-color-themePrimary;
$menuDividerColor: $ms-color-neutralLight;
/*** Lists ***/
$listBackgroundColor: transparent;
$listTextColor: $ms-color-neutralPrimary;
$listItemHoverColor: $ms-color-neutralLighter;
$listItemSelectedColor: $ms-color-neutralQuaternary;
$listItemSelectedHoverColor: $ms-color-neutralQuaternaryAlt;

View File

@ -0,0 +1,15 @@
.container{
.title {
font-size: 18px;
}
.description {
margin-top: 20px;
margin-bottom: 20px;
}
.button {
margin-bottom: 20px;
}
}

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import { IWebPartContext} from '@microsoft/sp-webpart-base';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import styles from './ConfigureWebPart.module.scss';
export interface IConfigureWebPartProps {
webPartContext: IWebPartContext;
title: string;
description?: string;
buttonText?: string;
}
const ConfigureWebPart: React.SFC<IConfigureWebPartProps> = (props) => {
const {
webPartContext,
title,
description,
buttonText,
} = props;
return (
<div className={styles.container}>
<div className={styles.title}>{title}</div>
<div className={styles.description}>
<MessageBar messageBarType={MessageBarType.info} >
{description ? description : 'Please configure this web part\'s properties first.'}
</MessageBar>
</div>
<div className={styles.button}>
<PrimaryButton iconProps={ { iconName: 'Edit' } } onClick={ (e) => {e.preventDefault(); webPartContext.propertyPane.open(); } }>
{buttonText ? buttonText : 'Configure Web Part'}
</PrimaryButton>
</div>
</div>
);
};
export default ConfigureWebPart;

View File

@ -0,0 +1,8 @@
/**
* Determines the display mode of the given control or form.
*/
export enum ControlMode {
Display = 1,
Edit = 2,
New = 3,
}

View File

@ -0,0 +1,12 @@
import { ControlMode } from '../datatypes/ControlMode';
import { IFieldSchema } from './datatypes/RenderListData';
export interface IListFormService {
getFieldSchemasForForm: (webUrl: string, listUrl: string, formType: ControlMode) => Promise<IFieldSchema[]>;
getDataForForm: (webUrl: string, listUrl: string, itemId: number, formType: ControlMode) => Promise<any>;
updateItem: (webUrl: string, listUrl: string, itemId: number,
fieldsSchema: IFieldSchema[],
data: any, originalData: any) => Promise<any>;
createItem: (webUrl: string, listUrl: string, fieldsSchema: IFieldSchema[], data: any) => Promise<any>;
}

View File

@ -0,0 +1,247 @@
import { Text } from '@microsoft/sp-core-library';
import { ISPHttpClientOptions, SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import * as strings from 'servicesStrings';
import { ControlMode } from '../datatypes/ControlMode';
import { IFieldSchema, RenderListDataOptions } from './datatypes/RenderListData';
import { IListFormService } from './IListFormService';
export class ListFormService implements IListFormService {
private spHttpClient: SPHttpClient;
constructor(spHttpClient: SPHttpClient) {
this.spHttpClient = spHttpClient;
}
/**
* Gets the schema for all relevant fields for a specified SharePoint list form.
*
* @param webUrl The absolute Url to the SharePoint site.
* @param listUrl The server-relative Url to the SharePoint list.
* @param formType The type of form (Display, New, Edit)
* @returns Promise object represents the array of field schema for all relevant fields for this list form.
*/
public getFieldSchemasForForm( webUrl: string, listUrl: string, formType: ControlMode ): Promise<IFieldSchema[]> {
return new Promise<IFieldSchema[]>((resolve, reject) => {
const httpClientOptions: ISPHttpClientOptions = {
headers: {
'Accept': 'application/json;odata=verbose',
'Content-type': 'application/json;odata=verbose',
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
'odata-version': '',
},
body: JSON.stringify({
parameters: {
__metadata: {
type: 'SP.RenderListDataParameters',
},
ViewXml: '<View><ViewFields><FieldRef Name="ID"/></ViewFields></View>',
RenderOptions: RenderListDataOptions.clientFormSchema,
},
}),
};
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/RenderListDataAsStream`
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}`;
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions)
.then((response: SPHttpClientResponse) => {
if (response.ok) {
return response.json();
} else {
reject( this.getErrorMessage(webUrl, response) );
}
})
.then((data) => {
const form = (formType === ControlMode.New) ? data.ClientForms.New : data.ClientForms.Edit;
resolve( form[ Object.keys( form )[0] ] );
})
.catch((error) => {
reject( this.getErrorMessage(webUrl, error) );
});
});
}
/**
* Retrieves the data for a specified SharePoint list form.
*
* @param webUrl The absolute Url to the SharePoint site.
* @param listUrl The server-relative Url to the SharePoint list.
* @param itemId The ID of the list item to be updated.
* @param formType The type of form (Display, New, Edit)
* @returns Promise representing an object containing all the field values for the list item.
*/
public getDataForForm( webUrl: string, listUrl: string, itemId: number, formType: ControlMode ): Promise<any> {
if (!listUrl || (!itemId) || (itemId === 0)) {
return Promise.resolve({}); // no data, so returns empty
}
return new Promise<any>((resolve, reject) => {
const httpClientOptions: ISPHttpClientOptions = {
headers: {
'Accept': 'application/json;odata=verbose',
'Content-type': 'application/json;odata=verbose',
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
'odata-version': '',
},
};
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/RenderExtendedListFormData`
+ `(itemId=${itemId},formId='editform',mode='2',options=7)`
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}`;
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions)
.then((response: SPHttpClientResponse) => {
if (response.ok) {
return response.json();
} else {
reject( this.getErrorMessage(webUrl, response) );
}
})
.then((data) => {
const extendedData = JSON.parse(data.d.RenderExtendedListFormData);
if (formType !== ControlMode.Display) {
resolve(extendedData.ListData);
} else {
resolve(extendedData.Data.Row[0]);
}
})
.catch((error) => {
reject( this.getErrorMessage(webUrl, error) );
});
});
}
/**
* Saves the given data to the specified SharePoint list item.
*
* @param webUrl The absolute Url to the SharePoint site.
* @param listUrl The server-relative Url to the SharePoint list.
* @param itemId The ID of the list item to be updated.
* @param fieldsSchema The array of field schema for all relevant fields of this list.
* @param data An object containing all the field values to update.
* @param originalData An object containing all the field values retrieved on loading from list item.
* @returns Promise object represents the updated or erroneous form field values.
*/
public updateItem( webUrl: string, listUrl: string, itemId: number,
fieldsSchema: IFieldSchema[], data: any, originalData: any ): Promise<any> {
return new Promise<any>((resolve, reject) => {
const httpClientOptions: ISPHttpClientOptions = {
headers: {
'Accept': 'application/json;odata=verbose',
'Content-type': 'application/json;odata=verbose',
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
'odata-version': '',
},
};
const formValues = this.GetFormValues(fieldsSchema, data, originalData);
httpClientOptions.body = JSON.stringify({
bNewDocumentUpdate: false,
checkInComment: null,
formValues,
});
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/items(@itemId)/ValidateUpdateListItem()`
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}&@itemId=%27${itemId}%27`;
this.spHttpClient.post(endpoint, SPHttpClient.configurations.v1, httpClientOptions )
.then((response: SPHttpClientResponse) => {
if (response.ok) {
return response.json();
} else {
reject( this.getErrorMessage(webUrl, response) );
}
})
.then((respData) => {
resolve( respData.d.ValidateUpdateListItem.results );
})
.catch((error) => {
reject( this.getErrorMessage(webUrl, error) );
});
});
}
/**
* Adds a new SharePoint list item to a list using the given data.
*
* @param webUrl The absolute Url to the SharePoint site.
* @param listUrl The server-relative Url to the SharePoint list.
* @param fieldsSchema The array of field schema for all relevant fields of this list.
* @param data An object containing all the field values to set on creating item.
* @returns Promise object represents the updated or erroneous form field values.
*/
public createItem( webUrl: string, listUrl: string, fieldsSchema: IFieldSchema[], data: any ): Promise<any> {
return new Promise<any>((resolve, reject) => {
const formValues = this.GetFormValues(fieldsSchema, data, {});
const httpClientOptions: ISPHttpClientOptions = {
headers: {
'Accept': 'application/json;odata=verbose',
'Content-type': 'application/json;odata=verbose',
'X-SP-REQUESTRESOURCES': 'listUrl=' + encodeURIComponent(listUrl),
'odata-version': '',
},
body: JSON.stringify({
listItemCreateInfo: {
__metadata: { type: 'SP.ListItemCreationInformationUsingPath' },
FolderPath: {
__metadata: { type: 'SP.ResourcePath' },
DecodedUrl: listUrl,
},
},
formValues,
bNewDocumentUpdate: false,
checkInComment: null,
}),
};
const endpoint = `${webUrl}/_api/web/GetList(@listUrl)/AddValidateUpdateItemUsingPath`
+ `?@listUrl=${encodeURIComponent('\'' + listUrl + '\'')}`;
this.spHttpClient.post( endpoint, SPHttpClient.configurations.v1, httpClientOptions )
.then( (response: SPHttpClientResponse) => {
if (response.ok) {
return response.json();
} else {
reject(this.getErrorMessage(webUrl, response));
}
})
.then((respData) => {
resolve( respData.d.AddValidateUpdateItemUsingPath.results );
})
.catch((error) => {
reject( this.getErrorMessage(webUrl, error) );
});
});
}
private GetFormValues( fieldsSchema: IFieldSchema[], data: any, originalData: any )
: Array<{ FieldName: string, FieldValue: any, HasException: boolean, ErrorMessage: string }> {
return fieldsSchema.filter(
(field) => (
(!field.ReadOnlyField)
&& (field.InternalName in data)
&& (data[field.InternalName] !== null)
&& (data[field.InternalName] !== originalData[field.InternalName])
),
)
.map( (field) => {
return {
ErrorMessage: null,
FieldName: field.InternalName,
FieldValue: data[field.InternalName],
HasException: false,
};
},
);
}
/**
* Returns an error message based on the specified error object
* @param error : An error string/object
*/
private getErrorMessage( webUrl: string, error: any ): string {
let errorMessage: string = error.statusText ? error.statusText : error.statusMessage ? error.statusMessage : error;
const serverUrl = `{window.location.protocol}//{window.location.hostname}`;
const webServerRelativeUrl = webUrl.replace(serverUrl, '');
if (error.status === 403) {
errorMessage = Text.format(strings.ErrorWebAccessDenied, webServerRelativeUrl);
} else if (error.status === 404) {
errorMessage = Text.format(strings.ErrorWebNotFound, webServerRelativeUrl);
}
return errorMessage;
}
}

View File

@ -0,0 +1,34 @@
import { Text } from '@microsoft/sp-core-library';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
export class ListService {
private spHttpClient: SPHttpClient;
constructor(spHttpClient: SPHttpClient) {
this.spHttpClient = spHttpClient;
}
public getListsFromWeb(webUrl: string): Promise<Array<{url: string, title: string}>> {
return new Promise<Array<{url: string, title: string}>>((resolve, reject) => {
const endpoint = Text.format("{0}/_api/web/lists?$select=Title,RootFolder/ServerRelativeUrl&$filter=(IsPrivate eq false) and (IsCatalog eq false) and (Hidden eq false)&$expand=RootFolder", webUrl);
this.spHttpClient.get(endpoint, SPHttpClient.configurations.v1).then((response: SPHttpClientResponse) => {
if (response.ok) {
response.json().then((data: any) => {
const listTitles: Array<{url: string, title: string}> = data.value.map((list) => {
return {url: list.RootFolder.ServerRelativeUrl, title: list.Title};
});
resolve( listTitles.sort( (a, b) => a.title.localeCompare(b.title)) );
})
.catch((error) => { reject(error); });
} else {
reject(response);
}
})
.catch((error) => { reject(error); });
});
}
}

View File

@ -0,0 +1,120 @@
export enum RenderListDataOptions {
none = 0,
contextInfo = 1,
listData = 2,
listSchema = 4,
menuView = 8,
listContentType= 16,
fileSystemItemId= 32,
clientFormSchema= 64,
quickLaunch= 128,
spotlight= 256,
visualization= 512,
viewMetadata= 1024,
disableAutoHyperlink= 2048,
enableMediaTAUrls= 4096,
parentInfo= 8192,
}
export interface IChoice {
LookupId: number;
LookupValue: string;
}
export interface IFieldSchema {
Id: string;
Title: string;
InternalName: string;
StaticName: string;
Hidden: boolean;
IMEMode: string;
Name: string;
Required: boolean;
Direction: string;
FieldType: string;
Description: string;
ReadOnlyField: boolean;
IsAutoHyperLink: boolean;
Type: string;
DefaultValue: string;
DefaultValueTyped: any;
MaxLength: number;
DependentLookup?: boolean;
AllowMultipleValues?: boolean;
BaseDisplayFormUrl: string;
Throttled?: boolean;
LookupListId: string;
ChoiceCount?: number;
Choices: any[];
RichText?: boolean;
AppendOnly?: boolean;
RichTextMode?: number;
NumberOfLines?: number;
AllowHyperlink?: boolean;
RestrictedMode?: boolean;
ScriptEditorAdderId: string;
FillInChoice?: boolean;
MultiChoices: string[];
FormatType?: number;
ShowAsPercentage?: boolean;
Presence?: boolean;
WithPicture?: boolean;
DefaultRender?: boolean;
WithPictureDetail?: boolean;
ListFormUrl: string;
UserDisplayUrl: string;
EntitySeparator: string;
PictureOnly?: boolean;
PictureSize?: any;
UserInfoListId: string;
SharePointGroupID?: number;
PrincipalAccountType: string;
SearchPrincipalSource?: number;
ResolvePrincipalSource?: number;
UserNoQueryPermission?: boolean;
DisplayFormat?: number;
CalendarType?: number;
ShowWeekNumber?: boolean;
TimeSeparator: string;
TimeZoneDifference: string;
FirstDayOfWeek?: number;
FirstWeekOfYear?: number;
HijriAdjustment?: number;
WorkWeek: string;
LocaleId: string;
LanguageId: string;
MinJDay?: number;
MaxJDay?: number;
DefaultValueFormatted: string;
SspId: string;
TermSetId: string;
AnchorId: string;
AllowFillIn?: boolean;
WidthCSS: string;
Lcid?: number;
IsSpanTermSets?: boolean;
IsSpanTermStores?: boolean;
IsAddTerms?: boolean;
IsUseCommaAsDelimiter?: boolean;
Disable?: boolean;
WebServiceUrl: string;
HiddenListInternalName: string;
}
export interface IFormSchema {
Item: IFieldSchema[];
}
export interface IRenderListDataAsStreamResponse {
ClientForms: {
New: IFormSchema;
Edit: IFormSchema;
};
ContentTypeIdToNameMap: any;
EnableAttachments: string;
FormRenderModes: {
New?: { RenderType: number };
Edit?: { RenderType: number };
Display?: { RenderType: number };
};
}

View File

@ -0,0 +1,6 @@
define([], function() {
return {
ErrorWebAccessDenied: "You do not have access to the previously configured web url '{0}'. Either leave the WebPart properties as is or select another web url.",
ErrorWebNotFound: "The previously configured web url '{0}' is not found anymore. Either leave the WebPart properties as is or select another web url.",
}
});

View File

@ -0,0 +1,10 @@
declare interface IServicesStrings {
ErrorWebAccessDenied: string;
ErrorWebNotFound: string;
}
declare module 'servicesStrings' {
const strings: IServicesStrings;
export = strings;
}

View File

@ -0,0 +1,5 @@
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
export interface IPropertyPaneAsyncDropdownInternalProps extends IPropertyPaneAsyncDropdownProps, IPropertyPaneCustomFieldProps {
}

View File

@ -0,0 +1,9 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
export interface IPropertyPaneAsyncDropdownProps {
label: string;
loadOptions: () => Promise<IDropdownOption[]>;
onPropertyChange: (propertyPath: string, newValue: any) => void;
selectedKey: string | number;
disabled?: boolean;
}

View File

@ -0,0 +1,60 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import {
IPropertyPaneField,
PropertyPaneFieldType
} from '@microsoft/sp-webpart-base';
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
import { IPropertyPaneAsyncDropdownInternalProps } from './IPropertyPaneAsyncDropdownInternalProps';
import AsyncDropdown from './components/AsyncDropdown';
import { IAsyncDropdownProps } from './components/IAsyncDropdownProps';
export class PropertyPaneAsyncDropdown implements IPropertyPaneField<IPropertyPaneAsyncDropdownProps> {
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
public targetProperty: string;
public properties: IPropertyPaneAsyncDropdownInternalProps;
private elem: HTMLElement;
constructor(targetProperty: string, properties: IPropertyPaneAsyncDropdownProps) {
this.targetProperty = targetProperty;
this.properties = {
key: properties.label,
label: properties.label,
loadOptions: properties.loadOptions,
onPropertyChange: properties.onPropertyChange,
selectedKey: properties.selectedKey,
disabled: properties.disabled,
onRender: this.onRender.bind(this)
};
}
public render(): void {
if (!this.elem) {
return;
}
this.onRender(this.elem);
}
private onRender(elem: HTMLElement): void {
if (!this.elem) {
this.elem = elem;
}
const element: React.ReactElement<IAsyncDropdownProps> = React.createElement(AsyncDropdown, {
label: this.properties.label,
loadOptions: this.properties.loadOptions,
onChanged: this.onChanged.bind(this),
selectedKey: this.properties.selectedKey,
disabled: this.properties.disabled,
// required to allow the component to be re-rendered by calling this.render() externally
stateKey: new Date().toString()
});
ReactDom.render(element, elem);
}
private onChanged(option: IDropdownOption, index?: number): void {
this.properties.onPropertyChange(this.targetProperty, option.key);
}
}

View File

@ -0,0 +1,90 @@
import * as React from 'react';
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
import { Spinner } from 'office-ui-fabric-react/lib/components/Spinner';
import { IAsyncDropdownProps } from './IAsyncDropdownProps';
import { IAsyncDropdownState } from './IAsyncDropdownState';
export default class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> {
private selectedKey: React.ReactText;
constructor(props: IAsyncDropdownProps, state: IAsyncDropdownState) {
super(props);
this.selectedKey = props.selectedKey;
this.state = {
loading: false,
options: undefined,
error: undefined
};
}
public componentDidMount(): void {
this.loadOptions();
}
public componentDidUpdate(prevProps: IAsyncDropdownProps, prevState: IAsyncDropdownState): void {
if (this.props.disabled !== prevProps.disabled ||
this.props.stateKey !== prevProps.stateKey) {
this.loadOptions();
}
}
public render(): JSX.Element {
const loading = this.state.loading;
const error: JSX.Element = this.state.error !== undefined ? <div className={'ms-TextField-errorMessage ms-u-slideDownIn20'}>Error while loading items: {this.state.error}</div> : <div />;
return (
<div>
<Dropdown label={this.props.label}
disabled={this.props.disabled || this.state.loading || this.state.error !== undefined}
onChanged={this.onChanged.bind(this)}
selectedKey={this.selectedKey}
options={this.state.options}
{...loading ? {onRenderCaretDown: () => <Spinner />} : {}} />
{error}
</div>
);
}
private loadOptions(): void {
this.setState({
loading: true,
error: undefined,
options: undefined
});
this.props.loadOptions()
.then((options: IDropdownOption[]): void => {
this.setState({
loading: false,
error: undefined,
options: options
});
}, (error: any): void => {
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
prevState.loading = false;
prevState.error = error;
return prevState;
});
});
}
private onChanged(option: IDropdownOption, index?: number): void {
this.selectedKey = option.key;
// reset previously selected options
const options: IDropdownOption[] = this.state.options;
options.forEach((o: IDropdownOption): void => {
if (o.key !== option.key) {
o.selected = false;
}
});
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
prevState.options = options;
return prevState;
});
if (this.props.onChanged) {
this.props.onChanged(option, index);
}
}
}

View File

@ -0,0 +1,10 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
export interface IAsyncDropdownProps {
label: string;
loadOptions: () => Promise<IDropdownOption[]>;
onChanged: (option: IDropdownOption, index?: number) => void;
selectedKey: string | number;
disabled: boolean;
stateKey: string;
}

View File

@ -0,0 +1,7 @@
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
export interface IAsyncDropdownState {
loading: boolean;
options: IDropdownOption[];
error: string;
}

View File

@ -0,0 +1,13 @@
import { ControlMode } from '../../common/datatypes/ControlMode';
import { IFieldConfiguration } from './components/IFieldConfiguration';
export interface IListFormWebPartProps {
title: string;
description: string;
listUrl: string;
formType: ControlMode;
itemId?: string;
showUnsupportedFields: boolean;
redirectUrl?: string;
fields?: IFieldConfiguration[];
}

View File

@ -0,0 +1,29 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "48e2d130-7eb7-4ee9-aa23-5ddbdfd175b1",
"alias": "ListFormWebPart",
"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": "48e2d130-7eb7-4ee9-aa23-5ddbdfd175b1",
"group": { "default": "Under Development" },
"title": { "default": "List Form" },
"description": { "default": "Shows a form for the selected list" },
"officeFabricIconFontName": "PreviewLink",
"properties": {
"title": "List Form",
"description": "",
"listUrl": "",
"formType": 3
}
}]
}

View File

@ -0,0 +1,224 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { DisplayMode, Environment, EnvironmentType, Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'ListFormWebPartStrings';
import ListForm from './components/ListForm';
import { IListFormProps } from './components/IListFormProps';
import { IListFormWebPartProps } from './IListFormWebPartProps';
import { IFieldConfiguration } from './components/IFieldConfiguration';
import { PropertyPaneAsyncDropdown } from '../../controls/PropertyPaneAsyncDropdown/PropertyPaneAsyncDropdown';
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import ConfigureWebPart from '../../common/components/ConfigureWebPart';
import { update, get } from '@microsoft/sp-lodash-subset';
import { ListService } from '../../common/services/ListService';
import { ControlMode } from '../../common/datatypes/ControlMode';
export default class ListFormWebPart extends BaseClientSideWebPart<IListFormWebPartProps> {
private listService: ListService;
private cachedLists = null;
protected onInit(): Promise<void> {
return super.onInit().then( _ => {
this.listService = new ListService(this.context.spHttpClient);
});
}
public render(): void {
let itemId;
if (this.properties.itemId) {
itemId = Number(this.properties.itemId);
if (isNaN(itemId)) {
// if item Id is not a number we assume it is a query string parameter
const urlParams = new URLSearchParams(window.location.search);
itemId = Number(urlParams.get(this.properties.itemId));
}
}
let element;
if (Environment.type === EnvironmentType.Local) {
// show message that local worbench is not supported
element = React.createElement(
MessageBar,
{messageBarType: MessageBarType.blocked},
strings.LocalWorkbenchUnsupported
);
} else if (this.properties.listUrl) {
// show actual list form react component
element = React.createElement(
ListForm,
{
inDesignMode: this.displayMode === DisplayMode.Edit ,
spHttpClient: this.context.spHttpClient,
title: this.properties.title,
description: this.properties.description,
webUrl: this.context.pageContext.web.absoluteUrl,
listUrl: this.properties.listUrl,
formType: this.properties.formType,
id: itemId,
fields: this.properties.fields,
showUnsupportedFields: this.properties.showUnsupportedFields,
onSubmitSucceeded: (id: number) => this.formSubmitted(id),
onUpdateFields: (fields: IFieldConfiguration[]) => this.updateField(fields),
}
);
} else {
// show configure web part react component
element = React.createElement(
ConfigureWebPart,
{
webPartContext: this.context,
title: this.properties.title,
description: strings.MissingListConfiguration,
buttonText: strings.ConfigureWebpartButtonText
}
);
}
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
const mainGroup = {
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title', {
label: strings.TitleFieldLabel
}),
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel,
multiline: true
}),
new PropertyPaneAsyncDropdown('listUrl', {
label: strings.ListFieldLabel,
loadOptions: this.loadLists.bind(this),
onPropertyChange: this.onListChange.bind(this),
selectedKey: this.properties.listUrl
}),
PropertyPaneDropdown('formType', {
label: strings.FormTypeFieldLabel,
options: Object.keys(ControlMode)
.map( (k) => ControlMode[k]).filter( (v) => typeof v === 'string' )
.map( (n) => ({key: ControlMode[n], text: n}) ),
disabled: !this.properties.listUrl
}),
]
};
if (this.properties.formType !== ControlMode.New) {
mainGroup.groupFields.push(
PropertyPaneTextField( 'itemId', {
label: strings.ItemIdFieldLabel,
deferredValidationTime: 2000,
description: strings.ItemIdFieldDescription
}));
}
mainGroup.groupFields.push(
PropertyPaneToggle('showUnsupportedFields', {
label: strings.ShowUnsupportedFieldsLabel,
disabled: !this.properties.listUrl
})
);
mainGroup.groupFields.push(
PropertyPaneTextField('redirectUrl', {
label: strings.RedirectUrlFieldLabel,
description: strings.RedirectUrlFieldDescription,
disabled: !this.properties.listUrl
})
);
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [mainGroup]
}
]
};
}
private loadLists(): Promise<IDropdownOption[]> {
return new Promise<IDropdownOption[]>((resolve: (options: IDropdownOption[]) => void, reject: (error: any) => void) => {
if (Environment.type === EnvironmentType.Local) {
resolve( [{
key: 'sharedDocuments',
text: 'Shared Documents',
},
{
key: 'someList',
text: 'Some List',
}] );
} else if (Environment.type === EnvironmentType.SharePoint ||
Environment.type === EnvironmentType.ClassicSharePoint) {
try {
if (!this.cachedLists) {
return this.listService.getListsFromWeb(this.context.pageContext.web.absoluteUrl)
.then( (lists) => {
this.cachedLists = lists.map( (l) => ({ key: l.url, text: l.title } as IDropdownOption) );
resolve( this.cachedLists );
} );
} else {
// using cached lists if available to avoid loading spinner every time property pane is refreshed
return resolve( this.cachedLists );
}
} catch (error) {
alert( strings.ErrorOnLoadingLists + error );
}
}
});
}
private onListChange(propertyPath: string, newValue: any): void {
const oldValue: any = get(this.properties, propertyPath);
if (oldValue !== newValue) {
this.properties.fields = null;
}
// store new value in web part properties
update( this.properties, propertyPath, (): any => newValue );
// refresh property Pane
this.context.propertyPane.refresh();
// refresh web part
this.render();
}
private updateField(fields: IFieldConfiguration[]): any {
this.properties.fields = fields;
// render web part again so that React List Form component is rerendered with changed fields
this.render();
}
private formSubmitted(id: number) {
if (this.properties.redirectUrl) {
// redirect to configured URL after successfully submitting form
window.location.href = this.properties.redirectUrl.replace('[ID]', id.toString() );
}
}
}

View File

@ -0,0 +1,66 @@
// import
@import '../../../common/theming';
.draggableComponent {
/*border: '1px dashed gray',
padding: '0.5rem 1rem',
marginBottom: '.5rem',*/
opacity: 1;
background-color: transparent;
position: relative;
}
.draggableComponent:hover{
background-color: $ms-color-neutralLighter;
outline: 1px solid;
outline-color: $ms-color-neutralLight;
}
.draggableComponent.isDragging {
opacity: 0.3;
background-color: $ms-color-themeLighter;
}
.toolbar {
position: absolute;
right: 0px;
top: 0px;
height: 100%;
min-width: 34px;
background-color: $ms-color-white;
color: $ms-color-themeLight;
display: none;
overflow: hidden;
text-align: center;
transition: all .3s;
transition-property: background-color,color;
.button {
height: 34px;
min-width: 34px;
display: block;
border: 1px solid transparent;
color: $ms-color-themeLight;
background-color: $ms-color-white;
padding: 0 8px;
overflow: hidden;
text-align: center;
transition: all .3s;
transition-property: background-color,color;
font-weight: 400;
font-size: 14px;
z-index: 10;
&:hover {
background-color: $ms-color-themeDark;
color: $ms-color-white;
}
}
}
.draggableComponent:hover .toolbar {
display: block;
}

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import { DragSource, DropTarget } from 'react-dnd';
import { findDOMNode } from 'react-dom';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import styles from './DraggableComponent.module.scss';
const dragSource = {
beginDrag(props: IDraggableComponentProps) {
return {
key: props.itemKey,
originalIndex: props.index,
};
},
endDrag(props: IDraggableComponentProps, monitor) {
const { key: droppedKey, originalIndex } = monitor.getItem();
const didDrop = monitor.didDrop();
if (!didDrop) {
props.moveField(droppedKey, originalIndex);
}
},
};
const dragTarget = {
hover(props: IDraggableComponentProps, monitor) {
const { key: draggedKey } = monitor.getItem();
if (draggedKey !== props.itemKey) {
props.moveField(draggedKey, props.index);
}
},
};
export interface IDraggableComponentProps {
index: number;
itemKey: string;
isDragging?: boolean;
connectDragSource?(child: any): any;
connectDropTarget?(child: any): any;
moveField(fieldKey: string, toIndex: number): void;
removeField(index: number): void;
}
@DropTarget('Fields', dragTarget, (connect) => ({
connectDropTarget: connect.dropTarget(),
}))
@DragSource('Fields', dragSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class DraggableComponent extends React.Component<IDraggableComponentProps> {
constructor(props) {
super(props);
}
public render() {
const { children, isDragging, connectDragSource, connectDropTarget } = this.props;
return connectDropTarget(connectDragSource(
<div className={css('ard-draggableComponent', styles.draggableComponent, isDragging ? styles.isDragging : null)}>
{children}
<div className={css(styles.toolbar)}>
<button type="button" className={css('ard-draggableComponent', styles.button)} title="Move field" ><i className="ms-Icon ms-Icon--Move"></i></button>
<button type="button" className={css('ard-draggableComponent', styles.button)} title="Remove field" onClick={() => this.props.removeField(this.props.index)}><i className="ms-Icon ms-Icon--Delete"></i></button>
</div>
</div>,
));
}
}

View File

@ -0,0 +1,4 @@
export interface IFieldConfiguration {
key: string;
fieldName: string;
}

View File

@ -0,0 +1,19 @@
import { SPHttpClient } from '@microsoft/sp-http';
import { ControlMode } from '../../../common/datatypes/ControlMode';
import { IFieldConfiguration } from './IFieldConfiguration';
export interface IListFormProps {
title: string;
description?: string;
webUrl: string;
listUrl: string;
formType: ControlMode;
id?: number;
fields?: IFieldConfiguration[];
spHttpClient: SPHttpClient;
inDesignMode?: boolean;
showUnsupportedFields?: boolean;
onSubmitSucceeded?(id: number): void;
onSubmitFailed?(fieldErrors: any): void;
onUpdateFields?(newFields: IFieldConfiguration[]): void;
}

View File

@ -0,0 +1,15 @@
import { IFieldSchema } from '../../../common/services/datatypes/RenderListData';
export interface IListFormState {
isLoadingSchema: boolean;
isLoadingData: boolean;
isSaving: boolean;
fieldsSchema?: IFieldSchema[];
data: any;
originalData: any;
errors: string[];
notifications: string[];
fieldErrors: {[fieldName: string]: string};
showUnsupportedFields?: boolean;
}

View File

@ -0,0 +1,82 @@
@import '../../../common/theming';
.listForm {
.title {
color: $bodyTextColor;
}
.description {
color: $bodySubtextColor;
display: inline-block;
margin: 10px 0;
font-weight: 100;
}
.formFieldsContainer{
padding-top: 10px;
padding-bottom: 10px;
&.isDataLoading{
opacity: 0.5;
}
}
.formButtonsContainer{
padding-top: 10px;
padding-bottom: 10px;
button {
margin-right: 4px;
}
}
.addFieldToolbox {
background: 0 0;
border: none;
cursor: pointer;
transition: all .3s ease;
width: 100%;
color: $ms-color-neutralTertiary;
&:hover, &:focus{
color: $ms-color-themePrimary;
}
.addFieldToolboxPlusButton {
left: calc(50% - 11.5px);
font-size: 24px;
}
i {
background-color: $bodyBackgroundColor;
padding-left: 2px;
}
:global(.ms-Button-menuIcon) {
display: none;
}
}
}
.listForm .addFieldToolbox:after,
:global(.ms-Fabric.is-focusVisible) .listForm :global(.ms-Button).addFieldToolbox:focus::after {
left: 0;
border-top: 1px dashed;
content: "";
height: 1px;
position: absolute;
top: 12px;
width: 100%;
transition: all .3s ease;
z-index: -1;
outline: 0;
}
.listForm .addFieldToolbox:focus:after {
color: $ms-color-themeDark;
}

View File

@ -0,0 +1,391 @@
import * as React from 'react';
import { autobind } from 'office-ui-fabric-react/lib/Utilities';
import { IFieldConfiguration } from './IFieldConfiguration';
import { IListFormProps } from './IListFormProps';
import { IListFormState } from './IListFormState';
import { ControlMode } from '../../../common/datatypes/ControlMode';
import { IListFormService } from '../../../common/services/IListFormService';
import { ListFormService } from '../../../common/services/ListFormService';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import SPFormField from './formFields/SPFormField';
import DraggableComponent from './DraggableComponent';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import * as strings from 'ListFormStrings';
import styles from './ListForm.module.scss';
/*************************************************************************************
* React Component to render a SharePoint list form on any page.
* The list form can be configured to be either a new form for adding a new list item,
* an edit form for changing an existing list item or a display form for showing the
* fields of an existing list item.
* In design mode the fields to render can be moved, added and deleted.
*************************************************************************************/
class ListForm extends React.Component<IListFormProps, IListFormState> {
private listFormService: IListFormService;
constructor( props: IListFormProps ) {
super(props);
// set initial state
this.state = {
isLoadingSchema: false,
isLoadingData: false,
isSaving: false,
data: {},
originalData: {},
errors: [],
notifications: [],
fieldErrors: {}
};
this.listFormService = new ListFormService(props.spHttpClient);
}
public render() {
let menuProps;
if (this.state.fieldsSchema) {
menuProps = {
shouldFocusOnMount: true,
directionalHint: DirectionalHint.topCenter,
items: this.state.fieldsSchema.map(
(fld) => ({ key: fld.InternalName, name: fld.Title, onClick: (ev, item) => this.appendField(fld.InternalName) })
)
};
}
return (
<div className={styles.listForm}>
<div className={css(styles.title, 'ms-font-xl')}>{this.props.title}</div>
{ (this.props.description) && <div className={styles.description}>{this.props.description}</div> }
{ this.renderNotifications() }
{ this.renderErrors() }
{ (!this.props.listUrl) ? <MessageBar messageBarType={MessageBarType.warning}>Please configure a list for this component first.</MessageBar> : '' }
{ (this.state.isLoadingSchema)
? (<Spinner size={ SpinnerSize.large } label={strings.LoadingFormIndicator} />)
: ((this.state.fieldsSchema) &&
<div>
<div className={css(styles.formFieldsContainer, this.state.isLoadingData ? styles.isDataLoading : null)}>
{ this.renderFields() }
{ this.props.inDesignMode &&
<DefaultButton aria-haspopup='true' aria-label={strings.AddNewFieldAction} className={styles.addFieldToolbox}
title={strings.AddNewFieldAction} menuProps={menuProps} data-is-focusable='false' >
<div className={styles.addFieldToolboxPlusButton}>
<i aria-hidden='true' className='ms-Icon ms-Icon--CircleAdditionSolid' />
</div>
</DefaultButton>
}
</div>
<div className={styles.formButtonsContainer}>
{(this.props.formType !== ControlMode.Display) &&
<PrimaryButton
disabled={ false }
text={strings.SaveButtonText}
onClick={ () => this.saveItem() }
/>
}
<DefaultButton
disabled={ false }
text={strings.CancelButtonText}
onClick={ () => this.readData(this.props.listUrl, this.props.formType, this.props.id) }
/>
</div>
</div>
)
}
</div>
);
}
private renderNotifications() {
if (this.state.notifications.length === 0) {
return null;
}
setTimeout( () => { this.setState({...this.state, notifications: []}); }, 4000 );
return <div>
{
this.state.notifications.map( (item, idx) =>
<MessageBar messageBarType={ MessageBarType.success }>{item}</MessageBar>
)
}
</div>;
}
private renderErrors() {
return this.state.errors.length > 0
?
<div>
{
this.state.errors.map( (item, idx) =>
<MessageBar
messageBarType={ MessageBarType.error }
isMultiline={ true }
onDismiss={ (ev) => this.clearError(idx) }
>
{item}
</MessageBar>
)
}
</div>
: null;
}
private renderFields() {
const { fieldsSchema, data, fieldErrors } = this.state;
const fields = this.getFields();
return (fields && (fields.length > 0))
?
<div className='ard-formFieldsContainer' >
{
fields.map((field, idx) => {
const fieldSchemas = fieldsSchema.filter((f) => f.InternalName === field.fieldName);
if (fieldSchemas.length > 0) {
const fieldSchema = fieldSchemas[0];
const value = data[field.fieldName];
let extraData;
if (data.hasOwnProperty(field.fieldName + '.')) {
extraData = data[field.fieldName + '.'];
} else {
extraData = Object.keys(data)
.filter( (propName) => propName.indexOf(field.fieldName + '.') === 0 )
.reduce( (newData, pn) => { newData[pn.substring(field.fieldName.length + 1)] = data[pn]; return newData; }, {} );
}
const errorMessage = fieldErrors[field.fieldName];
const fieldComponent = SPFormField({
fieldSchema: fieldSchema,
controlMode: this.props.formType,
value: value,
extraData: extraData,
errorMessage: errorMessage,
hideIfFieldUnsupported: !this.props.showUnsupportedFields,
valueChanged: (val) => this.valueChanged(field.fieldName, val) });
if (fieldComponent && this.props.inDesignMode) {
return (
<DraggableComponent
key={field.key}
index={idx}
itemKey={field.key}
moveField={(dragIdx, hoverIdx) => this.moveField(dragIdx, hoverIdx)}
removeField={(index) => this.removeField(index)} >
{fieldComponent}
</DraggableComponent>);
} else {
return fieldComponent;
}
}
})
}
</div>
: <MessageBar messageBarType={MessageBarType.warning}>No fields available!</MessageBar>;
}
public componentDidMount(): void {
this.readSchema(this.props.listUrl, this.props.formType).then(
() => this.readData(this.props.listUrl, this.props.formType, this.props.id)
);
}
public componentWillReceiveProps(nextProps: IListFormProps): void {
if ((this.props.listUrl !== nextProps.listUrl) || (this.props.formType !== nextProps.formType)) {
this.readSchema(nextProps.listUrl, nextProps.formType).then(
() => this.readData(nextProps.listUrl, nextProps.formType, nextProps.id)
);
} else if ((this.props.id !== nextProps.id) || (this.props.formType !== nextProps.formType)) {
this.readData(nextProps.listUrl, nextProps.formType, nextProps.id);
}
}
@autobind
private async readSchema(listUrl: string, formType: ControlMode): Promise<void> {
try {
if (!listUrl) {
this.setState({...this.state, isLoadingSchema: false, fieldsSchema: null, errors: [strings.ConfigureListMessage]});
return;
}
this.setState({ ...this.state, isLoadingSchema: true });
const fieldsSchema = await this.listFormService.getFieldSchemasForForm(
this.props.webUrl,
listUrl,
formType,
);
this.setState({ ...this.state, isLoadingSchema: false, fieldsSchema });
} catch (error) {
const errorText = `${strings.ErrorLoadingSchema}${listUrl}: ${error}`;
this.setState({
...this.state,
isLoadingSchema: false,
fieldsSchema: null,
errors: [...this.state.errors, errorText],
});
}
}
@autobind
private async readData(listUrl: string, formType: ControlMode, id?: number): Promise<void> {
try {
if ((formType === ControlMode.New) || !id) {
const data = this.state.fieldsSchema
.reduce( (newData, fld) => { newData[fld.InternalName] = fld.DefaultValue; return newData; }, {} );
this.setState({ ...this.state, data: data, originalData: {...data}, fieldErrors: {}, isLoadingData: false});
return;
}
this.setState({ ...this.state, data: {}, originalData: {}, fieldErrors: {}, isLoadingData: true});
const dataObj = await this.listFormService.getDataForForm( this.props.webUrl, listUrl, id, formType );
// We shallow clone here, so that changing values on dataObj object fields won't be changing in originalData too
const dataObjOriginal = { ...dataObj };
this.setState({...this.state, data: dataObj, originalData: dataObjOriginal, isLoadingData: false});
} catch (error) {
const errorText = `${strings.ErrorLoadingData}${id}: ${error}`;
this.setState({ ...this.state, data: {}, isLoadingData: false, errors: [...this.state.errors, errorText] });
}
}
@autobind
private valueChanged(fieldName: string, newValue: any) {
this.setState((prevState, props) => {
return {
...prevState,
data: {...prevState.data, [fieldName]: newValue},
fieldErrors: {
...prevState.fieldErrors,
[fieldName]:
(prevState.fieldsSchema.filter((item) => item.InternalName === fieldName)[0].Required) && !newValue
? strings.RequiredValueMessage
: ''
}
};
},
);
}
private async saveItem(): Promise<void> {
this.setState({ ...this.state, isSaving: true, errors: []});
try {
let updatedValues;
if (this.props.id) {
updatedValues = await this.listFormService.updateItem(
this.props.webUrl,
this.props.listUrl,
this.props.id,
this.state.fieldsSchema,
this.state.data,
this.state.originalData);
} else {
updatedValues = await this.listFormService.createItem(
this.props.webUrl,
this.props.listUrl,
this.state.fieldsSchema,
this.state.data);
}
let dataReloadNeeded = false;
const newState: IListFormState = {...this.state, fieldErrors: {}};
let hadErrors = false;
updatedValues.filter( (fieldVal) => fieldVal.HasException ).forEach( (element) => {
newState.fieldErrors[element.FieldName] = element.ErrorMessage;
hadErrors = true;
});
if (hadErrors) {
if (this.props.onSubmitFailed) {
this.props.onSubmitFailed(newState.fieldErrors);
} else {
newState.errors = [...newState.errors, strings.FieldsErrorOnSaving];
}
} else {
updatedValues.reduce(
(val, merged) => {
merged[val.FieldName] = merged[val.FieldValue]; return merged;
},
newState.data,
);
// we shallow clone here, so that changing values on state.data won't be changing in state.originalData too
newState.originalData = { ...newState.data };
let id = (this.props.id) ? this.props.id : 0;
if (id === 0) {
id = updatedValues.filter( (val) => val.FieldName === 'Id' )[0].FieldValue;
}
if (this.props.onSubmitSucceeded) { this.props.onSubmitSucceeded( id ); }
newState.notifications = [...newState.notifications, strings.ItemSavedSuccessfully];
dataReloadNeeded = true;
}
newState.isSaving = false;
this.setState(newState);
if (dataReloadNeeded) { this.readData(this.props.listUrl, this.props.formType, this.props.id); }
} catch (error) {
const errorText = strings.ErrorOnSavingListItem + error;
this.setState({ ...this.state, errors: [...this.state.errors, errorText] });
}
}
private clearError(idx: number) {
this.setState( (prevState, props) => {
return {...prevState, errors: prevState.errors.splice( idx, 1 )};
} );
}
private getFields(): IFieldConfiguration[] {
let fields = this.props.fields;
if ((!fields) && this.state.fieldsSchema) {
fields = this.state.fieldsSchema.map( (field) => ({key: field.InternalName, fieldName: field.InternalName}) );
}
return fields;
}
private appendField(fieldName: string) {
const newFields = this.getFields();
let fieldKey = fieldName;
let indexer = 0;
while (newFields.some( (fld) => fld.key === fieldKey )) {
indexer++;
fieldKey = fieldName + '_' + indexer;
}
newFields.push({key: fieldKey, fieldName: fieldName});
this.props.onUpdateFields(newFields);
}
private moveField(fieldKey, toIndex) {
const fields = this.getFields();
const dragField = fields.filter( (fld) => fld.key === fieldKey )[0];
const dragIndex = fields.indexOf(dragField);
const newFields = fields.splice(0); // clone
newFields.splice(dragIndex, 1);
newFields.splice(toIndex, 0, dragField);
this.props.onUpdateFields(newFields);
}
private removeField(index: number) {
const newFields = this.getFields().splice(0); // clone
newFields.splice(index, 1);
this.props.onUpdateFields(newFields);
}
}
export default DragDropContext(HTML5Backend)(ListForm);

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { DatePicker, DayOfWeek, IDatePickerProps, IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import * as strings from 'FormFieldStrings';
export interface IDateFormFieldProps extends IDatePickerProps {
locale: string;
}
export default class DateFormField extends React.Component<IDateFormFieldProps> {
public constructor() {
super();
}
public render() {
return (
<DatePicker
{...this.props}
parseDateFromString={ (dateStr: string) => new Date( Date.parse(dateStr) )}
formatDate={ (date: Date) => (typeof date.toLocaleDateString === 'function') ? date.toLocaleDateString(this.props.locale) : '' }
strings={strings}
/>
);
}
}

View File

@ -0,0 +1,37 @@
/** form field styles */
@import '../../../../common/theming';
.formField {
padding: 8px 3px;
position: relative;
.label {
width: 100%;
font-family: "Segoe UI Semibold WestEuropean","Segoe UI Semibold","Segoe UI",Tahoma,Arial,sans-serif;
font-size: 14px;
}
.label:global(.is-required) {
&::after {
content: ' *';
color: $ms-color-error;
}
}
.controlContainerDisplay {
display: inline-block;
width: calc(100% - 42px);
min-height: 20px;
vertical-align: top;
margin-right: 1px;
outline: 0;
padding: 0 0 5px;
font-size: 14px;
font-family: "Segoe UI Regular WestEuropean","Segoe UI",Tahoma,Arial,sans-serif;
font-weight: 400;
}
}

View File

@ -0,0 +1,79 @@
import * as React from 'react';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { css, DelayedRender } from 'office-ui-fabric-react/lib/Utilities';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { AnimationClassNames } from '@uifabric/styling';
import { ControlMode } from '../../../../common/datatypes/ControlMode';
import { IFieldSchema } from '../../../../common/services/datatypes/RenderListData';
import * as stylesImport from 'office-ui-fabric-react/lib/components/TextField/TextField.scss';
const styles: any = stylesImport;
import ardStyles from './FormField.module.scss';
export interface IFormFieldProps {
className?: string;
controlMode: ControlMode;
label?: string;
description?: string;
required?: boolean;
disabled?: boolean;
active?: boolean;
value: any;
errorMessage?: string;
valueChanged(newValue: any): void;
}
const FormField: React.SFC<IFormFieldProps> = (props) => {
const {
children,
className,
description,
disabled,
label,
required,
active,
errorMessage,
} = props;
const formFieldClassName = css('ard-formField', ardStyles.formField, styles.root, className, {
['is-required ' + styles.rootIsRequired]: required,
['is-disabled ' + styles.rootIsDisabled]: disabled,
['is-active ' + styles.rootIsActive]: active,
});
const isDescriptionAvailable = Boolean(props.description || props.errorMessage);
return (
<div className={ css(formFieldClassName, 'od-ClientFormFields-field') }>
<div className={ css('ard-FormField-wrapper', styles.wrapper) }>
{ label && <Label className={ css(ardStyles.label, {['is-required']: required}) } htmlFor={ this._id }>{ label }</Label> }
<div className={ css('ard-FormField-fieldGroup', ardStyles.controlContainerDisplay, active && styles.fieldGroupIsFocused, errorMessage && styles.invalid) }>
{children}
</div>
</div>
{ isDescriptionAvailable &&
<span>
{ description && <span className={ css('ard-FormField-description', styles.description) }>{ description }</span> }
{ errorMessage &&
<div aria-live='assertive'>
<DelayedRender>
<p className={ css('ard-FormField-errorMessage', AnimationClassNames.slideDownIn20, styles.errorMessage) }>
{ Icon({ iconName: 'Error', className: styles.errorIcon }) }
<span className={ styles.errorText } data-automation-id='error-message'>{ errorMessage }</span>
</p>
</DelayedRender>
</div>
}
</span>
}
</div>
);
};
export default FormField;

View File

@ -0,0 +1,52 @@
import * as React from 'react';
import { TextField, ITextFieldProps } from 'office-ui-fabric-react/lib/TextField';
import * as strings from 'FormFieldStrings';
export interface INumberFormFieldProps extends ITextFieldProps {
label?: string;
locale?: string;
value: string;
valueChanged(newValue: string): void;
}
export default class NumberFormField extends React.Component<INumberFormFieldProps, null> {
constructor(props) {
super(props);
this._validateNumber = this._validateNumber.bind(this);
}
public render(): JSX.Element {
// We need to set value to empty string when null or undefined to force TextField
// not to be used like an uncontrolled component and keep current value
const value = this.props.value ? this.props.value : '';
return (
<TextField
{...this.props}
className='NumberFormField'
label={ this.props.label }
value={value}
onChanged={ this.props.valueChanged }
onGetErrorMessage={ this._validateNumber }
/>
);
}
private _validateNumber(value: string): string {
return isNaN(this.parseNumber(value, this.props.locale))
? `${strings.InvalidNumberValue} ${value}`
: '';
}
private parseNumber(value, locale = navigator.language) {
const decimalSperator = Intl.NumberFormat(locale).format(1.1).charAt( 1 );
// const cleanPattern = new RegExp(`[^-+0-9${ example.charAt( 1 ) }]`, 'g');
const cleanPattern = new RegExp(`[${ '\' ,.'.replace(decimalSperator, '') }]`, 'g');
const cleaned = value.replace(cleanPattern, '');
const normalized = cleaned.replace(decimalSperator, '.');
return Number(normalized);
}
}

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import * as strings from 'FormFieldStrings';
const SPFieldBooleanEdit: React.SFC<ISPFormFieldProps> = (props) => {
return <Toggle
className='ard-booleanFormField'
checked={props.value === '1' || props.value === 'true' || props.value === 'Yes'}
onAriaLabel={strings.ToggleOnAriaLabel}
offAriaLabel={strings.ToggleOffAriaLabel}
onText={strings.ToggleOnText}
offText={strings.ToggleOffText}
onChanged={ (checked: boolean) => props.valueChanged(checked.toString())}
/>;
};
export default SPFieldBooleanEdit;

View File

@ -0,0 +1,47 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Dropdown, IDropdownProps, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import styles from './SPFormField.module.scss';
const SPFieldChoiceEdit: React.SFC<ISPFormFieldProps> = (props) => {
if (props.fieldSchema.FieldType !== "MultiChoice") {
const options = (props.fieldSchema.Required) ? props.fieldSchema.Choices : [''].concat(props.fieldSchema.Choices);
return <Dropdown
className={css(styles.dropDownFormField, 'ard-choiceFormField')}
options = {options.map( (option: string) => ({key: option, text: option}) )}
selectedKey = {props.value}
onChanged={ (item) => props.valueChanged( item.key.toString() ) }
/>;
} else {
const options = props.fieldSchema.MultiChoices;
const values = props.value ? props.value.split(';#').filter((s) => s) : [];
return <Dropdown
title = {JSON.stringify(props.fieldSchema) + props.value}
className={css(styles.dropDownFormField, 'ard-multiChoiceFormField')}
options = {options.map( (option: string) => ({key: option, text: option}) )}
selectedKeys = {values}
multiSelect
onChanged={ (item) => props.valueChanged( getUpdatedValue(values, item) ) }
/>;
}
};
function getUpdatedValue(oldValues: string[], changedItem: IDropdownOption): string {
const changedKey = changedItem.key.toString();
const newValues = [...oldValues];
if (changedItem.selected) {
// add option if it's checked
if (newValues.indexOf(changedKey) < 0) { newValues.push(changedKey); }
} else {
// remove the option if it's unchecked
const currIndex = newValues.indexOf(changedKey);
if (currIndex > -1) { newValues.splice(currIndex, 1); }
}
return newValues.join(';#');
}
export default SPFieldChoiceEdit;

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import * as moment from 'moment';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import { Locales } from '../../../../common/Locales';
import { ISPFormFieldProps } from './SPFormField';
import DateFormField from './DateFormField';
import * as strings from 'FormFieldStrings';
import styles from './SPFormField.module.scss';
const SPFieldDateEdit: React.SFC<ISPFormFieldProps> = (props) => {
const locale = Locales[props.fieldSchema.LocaleId];
return <DateFormField
{...props.value && moment(props.value).isValid() ? {value: moment(props.value).toDate()} : {}}
className={css(styles.dateFormField, 'ard-dateFormField')}
placeholder={strings.DateFormFieldPlaceholder}
isRequired={props.fieldSchema.Required}
ariaLabel={props.fieldSchema.Title}
locale={Locales[locale]}
firstDayOfWeek={props.fieldSchema.FirstDayOfWeek}
allowTextInput
onSelectDate={(date) => props.valueChanged(date.toLocaleDateString(locale))}
/>;
};
export default SPFieldDateEdit;

View File

@ -0,0 +1,16 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Link } from 'office-ui-fabric-react/lib/Link';
const SPFieldLookupDisplay: React.SFC<ISPFormFieldProps> = (props) => {
if ((props.value) && (props.value.length > 0)) {
const baseUrl = `${props.fieldSchema.BaseDisplayFormUrl}&ListId={${props.fieldSchema.LookupListId}}`;
return <div>
{props.value.map( (val) => <div><Link href={`{baseUrl}&ID=${val.lookupId}`}>{val.lookupValue}</Link></div> )}
</div>;
} else {
return <div></div>;
}
};
export default SPFieldLookupDisplay;

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Dropdown, IDropdownProps, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { css } from 'office-ui-fabric-react/lib/Utilities';
import * as strings from 'FormFieldStrings';
import styles from './SPFormField.module.scss';
const SPFieldLookupEdit: React.SFC<ISPFormFieldProps> = (props) => {
let options = props.fieldSchema.Choices.map( (option) => ({ key: option.LookupId, text: option.LookupValue }) );
if (props.fieldSchema.FieldType !== 'LookupMulti') {
if (!props.required) { options = [{key: 0, text: strings.LookupEmptyOptionText}].concat(options); }
const value = props.value ? Number(props.value.split(';#')[0]) : 0;
return <Dropdown
className={css(styles.dropDownFormField, 'ard-lookupFormField')}
options={options}
selectedKey={value}
onChanged={ (item) => props.valueChanged( `${item.key};#${item.text}` ) }
/>;
} else {
let values = [];
if (props.value) {
const splitArray = props.value.split(';#');
values = splitArray.filter( (item, idx) => (idx % 2 === 0) )
.map( (comp, idx) => ({key: Number(comp), text: (splitArray.length > idx + 1) ? splitArray[idx + 1] : '' }) );
}
return <Dropdown
className={css(styles.dropDownFormField, 'ard-lookupMultiFormField')}
options={options}
selectedKeys={values.map((val) => val.key)}
multiSelect
onChanged={ (item) => props.valueChanged( getUpdatedValue(values, item) ) }
/>;
}
};
function getUpdatedValue(oldValues: Array<{key: number, text: string}>, changedItem: IDropdownOption): string {
let newValues: Array<{key: number, text: string}>;
if (changedItem.selected) {
newValues = [...oldValues, {key: Number(changedItem.key), text: changedItem.text}];
} else {
newValues = oldValues.filter( (item) => item.key !== changedItem.key );
}
return newValues.reduce( (valStr, item) => valStr + `${item.key};#${item.text}`, '' );
}
export default SPFieldLookupEdit;

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import NumberFormField from './NumberFormField';
import * as strings from 'FormFieldStrings';
const SPFieldNumberEdit: React.SFC<ISPFormFieldProps> = (props) => {
return <NumberFormField
className='ard-numberFormField'
value={props.value}
valueChanged={props.valueChanged}
placeholder={strings.NumberFormFieldPlaceholder}
underlined
/>;
};
export default SPFieldNumberEdit;

View File

@ -0,0 +1,9 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
const SPFieldTextDisplay: React.SFC<ISPFormFieldProps> = (props) => {
const value = (props.value) ? ((typeof props.value === 'string') ? props.value : JSON.stringify(props.value)) : '';
return <span className='ard-textfield-display'>{value}</span>;
};
export default SPFieldTextDisplay;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import * as strings from 'FormFieldStrings';
const SPFieldTextEdit: React.SFC<ISPFormFieldProps> = (props) => {
// We need to set value to empty string when null or undefined to force TextField still be used like a controlled component
const value = props.value ? props.value : '';
return <TextField
className='ard-TextFormField'
name={props.fieldSchema.InternalName}
value={value}
onChanged={props.valueChanged}
placeholder={strings.TextFormFieldPlaceholder}
multiline={props.fieldSchema.FieldType === 'Note'}
underlined
noValidate
/>;
};
export default SPFieldTextEdit;

View File

@ -0,0 +1,17 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Link } from 'office-ui-fabric-react/lib/Link';
const SPFieldUrlDisplay: React.SFC<ISPFormFieldProps> = (props) => {
if (props.value) {
if (props.fieldSchema.DisplayFormat === 1) { // picture field
return <div><img src={props.value} title={(props.extraData) ? props.extraData.desc : ''}></img></div>;
} else {
return <div><Link target='_blank' href={props.value}>{(props.extraData && props.extraData.desc) ? props.extraData.desc : props.value}</Link></div>;
}
} else {
return <div></div>;
}
};
export default SPFieldUrlDisplay;

View File

@ -0,0 +1,14 @@
import * as React from 'react';
import { ISPFormFieldProps } from './SPFormField';
import { Link } from 'office-ui-fabric-react/lib/Link';
const SPFieldUserDisplay: React.SFC<ISPFormFieldProps> = (props) => {
if ((props.value) && (props.value.length > 0)) {
const baseUrl = `${props.fieldSchema.ListFormUrl}?PageType=4&ListId=${props.fieldSchema.UserInfoListId}`;
return <div>{props.value.map( (val) => <div><Link href={`{baseUrl}&ID=${val.id}`}>{val.title}</Link></div> )}</div>;
} else {
return <div></div>;
}
};
export default SPFieldUserDisplay;

View File

@ -0,0 +1,32 @@
@import '../../../../common/theming';
.dropDownFormField{
:global(.ms-Dropdown-title){
border: 0;
border-bottom: 1px solid;
border-color: $ms-color-neutralTertiary;
}
}
.dateFormField{
:global(.ms-TextField-fieldGroup){
border: 0;
border-bottom: 1px solid;
border-color: $ms-color-neutralTertiary;
}
}
.unsupportedFieldMessage{
color: $errorTextColor;
margin-top: 4px;
i{
margin-right: 4px;
top: 2px;
position: relative;
}
}

View File

@ -0,0 +1,123 @@
import * as React from 'react';
import { ControlMode } from '../../../../common/datatypes/ControlMode';
import { IFieldSchema } from '../../../../common/services/datatypes/RenderListData';
import FormField from './FormField';
import { IFormFieldProps } from './FormField';
import { IDatePickerStrings } from 'office-ui-fabric-react/lib/DatePicker';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import SPFieldTextEdit from './SPFieldTextEdit';
import SPFieldLookupEdit from './SPFieldLookupEdit';
import SPFieldChoiceEdit from './SPFieldChoiceEdit';
import SPFieldNumberEdit from './SPFieldNumberEdit';
import SPFieldDateEdit from './SPFieldDateEdit';
import SPFieldBooleanEdit from './SPFieldBooleanEdit';
import SPFieldTextDisplay from './SPFieldTextDisplay';
import SPFieldLookupDisplay from './SPFieldLookupDisplay';
import SPFieldUserDisplay from './SPFieldUserDisplay';
import SPFieldUrlDisplay from './SPFieldUrlDisplay';
import * as strings from 'FormFieldStrings';
import styles from './SPFormField.module.scss';
const EditFieldTypeMappings: {[fieldType: string]: React.StatelessComponent<ISPFormFieldProps>} = {
Text: SPFieldTextEdit,
Note: SPFieldTextEdit,
Lookup: SPFieldLookupEdit,
LookupMulti: SPFieldLookupEdit,
Choice: SPFieldChoiceEdit,
MultiChoice: SPFieldChoiceEdit,
Number: SPFieldNumberEdit,
Currency: SPFieldNumberEdit,
DateTime: SPFieldDateEdit,
Boolean: SPFieldBooleanEdit,
File: SPFieldTextEdit,
/* The following are known but unsupported types as of now:
User: null,
UserMulti: null,
URL: null,
TaxonomyFieldType: null,
Attachments: null,
TaxonomyFieldTypeMulti: null,
*/
};
const DisplayFieldTypeMappings: {[fieldType: string]: {component: React.StatelessComponent<ISPFormFieldProps>, valuePreProcess?: (value: any) => any}} = {
Text: { component: SPFieldTextDisplay },
Note: { component: SPFieldTextDisplay },
Lookup: { component: SPFieldLookupDisplay },
LookupMulti: { component: SPFieldLookupDisplay },
Choice: { component: SPFieldTextDisplay },
MultiChoice: {component: SPFieldTextDisplay, valuePreProcess: (val) => val ? val.join(', ') : '' },
Number: { component: SPFieldTextDisplay },
Currency: { component: SPFieldTextDisplay },
DateTime: { component: SPFieldTextDisplay },
Boolean: { component: SPFieldTextDisplay },
User: { component: SPFieldUserDisplay },
UserMulti: { component: SPFieldUserDisplay },
URL: { component: SPFieldUrlDisplay },
File: { component: SPFieldTextDisplay},
TaxonomyFieldType: { component: SPFieldTextDisplay, valuePreProcess: (val) => val ? val.Label : '' },
TaxonomyFieldTypeMulti: { component: SPFieldTextDisplay, valuePreProcess: (val) => val ? val.map( (v) => v.Label ).join(', ') : '' },
/* The following are known but unsupported types as of now:
Attachments: null,
*/
};
export interface ISPFormFieldProps extends IFormFieldProps {
extraData?: any;
fieldSchema: IFieldSchema;
hideIfFieldUnsupported?: boolean;
}
const SPFormField: React.SFC<ISPFormFieldProps> = (props) => {
let fieldControl = null;
const fieldType = props.fieldSchema.FieldType;
if (props.controlMode === ControlMode.Display) {
if (DisplayFieldTypeMappings.hasOwnProperty(fieldType)) {
const fieldMapping = DisplayFieldTypeMappings[fieldType];
const childProps = fieldMapping.valuePreProcess ? {...props, value: fieldMapping.valuePreProcess(props.value)} : props;
fieldControl = React.createElement( fieldMapping.component, childProps );
} else if (!props.hideIfFieldUnsupported) {
const value = (props.value) ? ((typeof props.value === 'string') ? props.value : JSON.stringify(props.value)) : '';
fieldControl = <div className={`ard-${fieldType}field-display`}>
<span>{value}</span>
<div className={styles.unsupportedFieldMessage}><Icon iconName='Error' />{`${strings.UnsupportedFieldType} "${fieldType}"`}</div>
</div>;
}
} else {
if (EditFieldTypeMappings.hasOwnProperty(fieldType)) {
fieldControl = React.createElement( EditFieldTypeMappings[fieldType], props );
} else if (!props.hideIfFieldUnsupported) {
const isObjValue = (props.value) && (typeof props.value !== 'string');
const value = (props.value) ? ((typeof props.value === 'string') ? props.value : JSON.stringify(props.value)) : '';
fieldControl = <TextField
readOnly
multiline={isObjValue}
value={value}
errorMessage={`${strings.UnsupportedFieldType} "${fieldType}"`}
underlined
/>;
}
}
return (fieldControl)
? <FormField
{...props}
label={props.label || props.fieldSchema.Title}
description={props.description || props.fieldSchema.Description}
required={props.fieldSchema.Required}
errorMessage={props.errorMessage}
>
{fieldControl}
</FormField>
: null;
};
export default SPFormField;

View File

@ -0,0 +1,28 @@
define([], function() {
return {
UnsupportedFieldType: "Unsupported field type",
InvalidNumberValue: "The value should be a number, actual is",
ToggleOnAriaLabel: "This toggle is checked. Press to uncheck.",
ToggleOffAriaLabel: "This toggle is unchecked. Press to check.",
ToggleOnText: "Yes",
ToggleOffText: "No",
TextFormFieldPlaceholder: "Enter text here",
DateFormFieldPlaceholder: "Enter a date",
NumberFormFieldPlaceholder: "Enter value here",
LookupEmptyOptionText: "(None)",
// IDatePickerStrings
months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
days: [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ],
shortDays: [ 'S', 'M', 'T', 'W', 'T', 'F', 'S' ],
goToToday: 'Go to today',
prevMonthAriaLabel: 'Go to previous month',
nextMonthAriaLabel: 'Go to next month',
prevYearAriaLabel: 'Go to previous year',
nextYearAriaLabel: 'Go to next year',
isRequiredErrorMessage: 'This date is required.',
invalidInputErrorMessage: 'Invalid date format.'
}
});

View File

@ -0,0 +1,31 @@
declare interface IFormFieldStrings {
UnsupportedFieldType: string;
InvalidNumberValue: string;
ToggleOnAriaLabel: string;
ToggleOffAriaLabel: string;
ToggleOnText: string;
ToggleOffText: string;
TextFormFieldPlaceholder: string;
DateFormFieldPlaceholder: string;
NumberFormFieldPlaceholder: string;
LookupEmptyOptionText: string;
// IDatePickerStrings
months: string[];
shortMonths: string[];
days: string[];
shortDays: string[];
goToToday: string;
isRequiredErrorMessage?: string;
invalidInputErrorMessage?: string;
prevMonthAriaLabel?: string;
nextMonthAriaLabel?: string;
prevYearAriaLabel?: string;
nextYearAriaLabel?: string;
}
declare module 'FormFieldStrings' {
const strings: IFormFieldStrings;
export = strings;
}

View File

@ -0,0 +1,15 @@
define([], function() {
return {
"SaveButtonText": "Save",
"CancelButtonText": "Cancel",
"AddNewFieldAction": "Add a new field to form",
"LoadingFormIndicator": "Loading the form...",
"ErrorLoadingSchema": "Error loading schema for list ",
"ConfigureListMessage": "Please configure a list in the web part's editor first.",
"RequiredValueMessage": "Please enter a value!",
"ErrorLoadingData": "Error loading data for item with ID ",
"ItemSavedSuccessfully": "Item saved successfully.",
"FieldsErrorOnSaving": 'The item could not be saved. Please check detailed error messages on the fields below.',
"ErrorOnSavingListItem": "Error on loading lists: ",
}
});

View File

@ -0,0 +1,19 @@
declare interface IListFormStrings {
SaveButtonText: string;
CancelButtonText: string;
AddNewFieldAction: string;
LoadingFormIndicator: string;
ErrorLoadingSchema: string;
ConfigureListMessage: string;
RequiredValueMessage: string;s
ErrorLoadingData: string;
ItemSavedSuccessfully: string;
FieldsErrorOnSaving: string;
ErrorOnSavingListItem: string;
}
declare module 'ListFormStrings' {
const strings: IListFormStrings;
export = strings;
}

View File

@ -0,0 +1,19 @@
define([], function() {
return {
"PropertyPaneDescription": "Configure the list form here. Once the list is configured fields can be moved, inserted and removed in the webpart's content itself.",
"BasicGroupName": "Settings",
"TitleFieldLabel": "Title",
"DescriptionFieldLabel": "Description",
"ListFieldLabel": "List",
"FormTypeFieldLabel": "Form Type",
"ItemIdFieldLabel": "Item ID",
"ItemIdFieldDescription" : "Enter either a number for the ID or the query string parameter name to use for the ID.",
"ShowUnsupportedFieldsLabel": "Show unsupported fields",
"RedirectUrlFieldLabel": "URL to redirect after saving (optional)",
"RedirectUrlFieldDescription": "Can contain [ID] as a placeholder to be replaced by ID of updated or created item. Example: /list/Test/DispForm.aspx?ID=[ID]",
"LocalWorkbenchUnsupported": "Running this web part in your local workbench is not supported. Please run it inside your SharePoint site.",
"MissingListConfiguration": "Please configure a SharePoint list in the web part's properties.",
"ConfigureWebpartButtonText": "Configure Web Part",
"ErrorOnLoadingLists": "Error on loading lists: ",
}
});

View File

@ -0,0 +1,22 @@
declare interface IListFormWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
TitleFieldLabel: string;
DescriptionFieldLabel: string;
ListFieldLabel: string;
FormTypeFieldLabel: string;
ItemIdFieldLabel: string;
ItemIdFieldDescription: string;
ShowUnsupportedFieldsLabel: string;
RedirectUrlFieldLabel: string;
RedirectUrlFieldDescription: string;
LocalWorkbenchUnsupported: string;
MissingListConfiguration: string;
ConfigureWebpartButtonText: string;
ErrorOnLoadingLists: string;
}
declare module 'ListFormWebPartStrings' {
const strings: IListFormWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('ListFormWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -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"
]
}
}

View File

@ -0,0 +1,15 @@
{
"enable": false,
"extends": "tslint:recommended",
"rules": {
"quotemark": [true, "single"],
"object-literal-sort-keys": [false],
"max-line-length": [true, 140],
"trailing-comma": [false],
"no-consecutive-blank-lines": [false],
"ordered-imports": [false],
"object-literal-shorthand": [false],
"member-ordering": [false]
},
"defaultSeverity": "warning"
}

View File

@ -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;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />