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:
parent
9c9807c4fd
commit
137526e5d4
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.2.0",
|
||||
"libraryName": "react-form-webpart",
|
||||
"libraryId": "373a20ef-dfc6-456a-95ec-171de3c94581",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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;
|
|
@ -0,0 +1,15 @@
|
|||
.container{
|
||||
.title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Determines the display mode of the given control or form.
|
||||
*/
|
||||
export enum ControlMode {
|
||||
Display = 1,
|
||||
Edit = 2,
|
||||
New = 3,
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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); });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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 };
|
||||
};
|
||||
}
|
|
@ -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.",
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
declare interface IServicesStrings {
|
||||
ErrorWebAccessDenied: string;
|
||||
ErrorWebNotFound: string;
|
||||
}
|
||||
|
||||
declare module 'servicesStrings' {
|
||||
const strings: IServicesStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';
|
||||
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
|
||||
|
||||
export interface IPropertyPaneAsyncDropdownInternalProps extends IPropertyPaneAsyncDropdownProps, IPropertyPaneCustomFieldProps {
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
|
||||
|
||||
export interface IAsyncDropdownState {
|
||||
loading: boolean;
|
||||
options: IDropdownOption[];
|
||||
error: string;
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export interface IFieldConfiguration {
|
||||
key: string;
|
||||
fieldName: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
28
samples/react-list-form/src/webparts/listForm/components/formFields/loc/en-us.js
vendored
Normal file
28
samples/react-list-form/src/webparts/listForm/components/formFields/loc/en-us.js
vendored
Normal 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.'
|
||||
|
||||
}
|
||||
});
|
31
samples/react-list-form/src/webparts/listForm/components/formFields/loc/formfieldstrings.d.ts
vendored
Normal file
31
samples/react-list-form/src/webparts/listForm/components/formFields/loc/formfieldstrings.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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: ",
|
||||
}
|
||||
});
|
19
samples/react-list-form/src/webparts/listForm/components/loc/listformstrings.d.ts
vendored
Normal file
19
samples/react-list-form/src/webparts/listForm/components/loc/listformstrings.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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: ",
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="mocha" />
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
describe('ListFormWebPart', () => {
|
||||
it('should do something', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
/// <reference path="@ms/odsp.d.ts" />
|
Loading…
Reference in New Issue