Add tooling packages from the FAST repository
# Pull Request ## 📖 Description <!--- Provide some background and a description of your work. --> This pull request adds the `@microsoft/fast-tooling`, `@microsoft/fast-tooling-react`, and `@microsoft/fast-tooling-wasm` packages to the repository using NPM 7 workspaces. This should allow: - Running the `npm run test` from the package root to run all tests - In the individual `fast-tooling` and `fast-tooling-react` packages running `npm start` to kick off a build of the `webpack-dev-server` ## 👩💻 Reviewer Notes <!--- Provide some notes for reviewers to help them provide targeted feedback and testing. --> Pull down the repository, use `npm i` or `npm install` to install dependencies, then use `npm run test` at the root and `npm start` in the `fast-tooling` and `fast-tooling-react` packages. Keep in mind that the requirements for this repository are now NPM 7 and NodeJS 16, ensure these are both installed. ## ✅ Checklist ### General <!--- Review the list and put an x in the boxes that apply. --> - [ ] I have added tests for my changes. - [x] I have tested my changes. - [x] I have updated the project documentation to reflect my changes.
This commit is contained in:
Родитель
0f18e5debd
Коммит
d39991bc75
|
@ -0,0 +1,11 @@
|
|||
# Never lint test files
|
||||
*.spec.ts
|
||||
|
||||
# Never lint node_modules
|
||||
node_modules
|
||||
|
||||
# Never lint build output
|
||||
dist
|
||||
|
||||
# Never lint coverage output
|
||||
coverage
|
|
@ -0,0 +1,75 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "latest",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-empty-interface": [
|
||||
"error",
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/interface-name-prefix": ["error", { prefixWithI: "never" }],
|
||||
"import/order": "error",
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
"comma-dangle": "off",
|
||||
"@typescript-eslint/typedef": [
|
||||
"error",
|
||||
{
|
||||
arrowParameter: false,
|
||||
arrayDestructuring: true,
|
||||
parameter: true,
|
||||
propertyDeclaration: true,
|
||||
memberVariableDeclaration: true,
|
||||
variableDeclarationIgnoreFunction: true,
|
||||
variableDeclaration: false,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
selector: "default",
|
||||
format: ["UPPER_CASE", "camelCase", "PascalCase"],
|
||||
leadingUnderscore: "allow",
|
||||
},
|
||||
{
|
||||
selector: "property",
|
||||
format: null, // disable for property names because of our foo__expanded convention for JSS
|
||||
// TODO: I think we can come up with a regex that ignores variables with __ in them
|
||||
},
|
||||
{
|
||||
selector: "variable",
|
||||
format: null, // disable for variable names because of our foo__expanded convention for JSS
|
||||
// TODO: I think we can come up with a regex that ignores variables with __ in them
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-fallthrough": "off",
|
||||
"no-unexpected-multiline": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
"react/no-children-prop": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
};
|
|
@ -55,9 +55,6 @@ storybook-static
|
|||
.tmp/
|
||||
temp/
|
||||
|
||||
# npm package-locks
|
||||
package-lock.json
|
||||
|
||||
# GitHub Actions Local Testing
|
||||
.github/workflows/testing/*.json
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
*.spec.ts
|
||||
*.spec.tsx
|
||||
**/__test-images__
|
||||
**/__tests__
|
||||
**/.tmp
|
||||
**/bootstrap
|
||||
**/coverage
|
||||
**/dist
|
||||
**/temp
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"printWidth": 90,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"endOfLine": "auto"
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
### Machine setup
|
||||
|
||||
To work with the FAST Tooling [monorepo](https://en.wikipedia.org/wiki/Monorepo) you'll need Git, Node.js, and NPM setup on your machine.
|
||||
To work with the FAST Tooling [monorepo](https://en.wikipedia.org/wiki/Monorepo) you'll need Git, Node.js@^16.0.0, and npm@^7.0.0 setup on your machine.
|
||||
|
||||
FAST Tooling uses Git as its source control system. If you haven't already installed it, you can download it [here](https://git-scm.com/downloads) or if you prefer a GUI-based approach, try [GitHub Desktop](https://desktop.github.com/).
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/* eslint-env node */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type,@typescript-eslint/no-var-requires,@typescript-eslint/typedef */
|
||||
/**
|
||||
* Utility for cleaning directories.
|
||||
* Usage: node build/clean.js %path%
|
||||
*/
|
||||
const path = require("path");
|
||||
const rimraf = require("rimraf");
|
||||
const argv = require("yargs").argv;
|
||||
|
||||
/**
|
||||
* All paths passed to the clean script
|
||||
*/
|
||||
const paths = argv._;
|
||||
|
||||
/**
|
||||
* Function to remove a given path
|
||||
*/
|
||||
function cleanPath(cleanPath) {
|
||||
if (!cleanPath) {
|
||||
console.error("No path specified.");
|
||||
process.exit(1);
|
||||
}
|
||||
const removePath = path.resolve(process.cwd(), cleanPath);
|
||||
rimraf(removePath, () => {
|
||||
console.log(removePath, "cleaned");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean all paths
|
||||
*/
|
||||
if (Array.isArray(paths)) {
|
||||
paths.forEach(cleanPath);
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "@microsoft/fast-tooling-repository",
|
||||
"description": "A system of development tools, and utilities used à la carte or as a suite to build enterprise-grade websites and applications.",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "Microsoft",
|
||||
"url": "https://discord.gg/FcSNfg4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"./packages/fast-tooling-react",
|
||||
"./packages/fast-tooling"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/microsoft/fast-tooling.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/microsoft/fast-tooling/issues/new/choose"
|
||||
},
|
||||
"scripts": {
|
||||
"test:diff:error": "echo \"Untracked files exist, try running npm prepare to identify the culprit.\" && exit 1",
|
||||
"test:diff": "git update-index --refresh && git diff-index --quiet HEAD -- || npm run test:diff:error",
|
||||
"test": "npm run prettier --workspaces --if-present && npm run test:diff && npm run test --workspaces --if-present"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,html}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.23.0",
|
||||
"@typescript-eslint/parser": "^2.23.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"chalk": "^2.4.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"dotenv": "^6.0.0",
|
||||
"glob": "^7.1.2",
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.1.2",
|
||||
"prettier": "2.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# don't ever lint node_modules
|
||||
node_modules
|
||||
# don't lint build output (make sure it's set to your correct build folder name)
|
||||
dist
|
||||
# don't lint coverage output
|
||||
coverage
|
||||
# don't lint tests
|
||||
__test__
|
||||
__tests__
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["../../.eslintrc"],
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
# App files
|
||||
app/
|
||||
|
||||
# Source files
|
||||
src/
|
||||
|
||||
# Tests
|
||||
__tests__/
|
||||
coverage/
|
||||
*.spec.*
|
||||
*.test.*
|
||||
|
||||
# Config files
|
||||
.prettierignore
|
||||
.eslintignore
|
||||
.eslintrc.cjs
|
||||
babel.config.js
|
||||
tsconfig.build.json
|
||||
tsconfig.json
|
||||
webpack.config.cjs
|
|
@ -0,0 +1 @@
|
|||
package-lock=false
|
|
@ -0,0 +1,2 @@
|
|||
coverage/*
|
||||
dist/*
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,895 @@
|
|||
# FAST Tooling React
|
||||
|
||||
The tooling available in FAST Tooling React can be used together to create UI for manipulating serializable data and viewing React components.
|
||||
|
||||
![JavaScript](https://img.shields.io/badge/ES6-Supported-yellow.svg?style=for-the-badge&logo=JavaScript) ![TypeScript](https://img.shields.io/badge/TypeScript-Supported-blue.svg?style=for-the-badge)
|
||||
|
||||
- [Benefits](#benefits)
|
||||
- [Concepts](#concepts)
|
||||
- [Ecosystem](#ecosystem)
|
||||
- [Installation](#installation)
|
||||
- [Requirements](#requirements)
|
||||
- [Form](#form)
|
||||
- [Validation](#validation)
|
||||
- [Drag and drop](#drag-and-drop)
|
||||
- [Using form control plugins](#using-form-control-plugins)
|
||||
- [List of control types](#list-of-control-types)
|
||||
- [Validation](#validation)
|
||||
- [JSON schema metadata](#json-schema-metadata)
|
||||
- [Title](#title)
|
||||
- [Description](#description)
|
||||
- [Disabled](#disabled)
|
||||
- [Examples & default](#examples-&-default)
|
||||
- [Badges](#badges)
|
||||
- [Alias](#alias)
|
||||
- [Dictionaries](#dictionaries)
|
||||
- [Categories](#categories)
|
||||
- [JSON schema keywords](#json-schema-keywords)
|
||||
- [oneOf & anyOf](#oneof-&-anyof)
|
||||
- [Enums](#enums)
|
||||
- [allOf & $ref](#allof-&-ref)
|
||||
- [Navigation](#navigation)
|
||||
- [Include data types](#include-data-types)
|
||||
- [Navigation Menu](#navigation-menu)
|
||||
- [Menu structure](#menu-structure)
|
||||
- [Expanding and Collapsing](#expanding-and-collapsing)
|
||||
- [Controlling the location](#controlling-the-location)
|
||||
- [Viewer](#viewer)
|
||||
- [Setting width and height](#setting-width-and-height)
|
||||
- [Sending custom messages](#sending-custom-messages)
|
||||
- [Receiving custom messages](#receiving-custom-messages)
|
||||
- [Select device](#select-device)
|
||||
- [Devices](#devices)
|
||||
- [Rotate](#rotate)
|
||||
- [Data utilities](#data-utilities)
|
||||
- [Transforming data](#transforming-data)
|
||||
|
||||
## Benefits
|
||||
|
||||
The FAST Tooling can be used in any combination for the following scenarios:
|
||||
- Mapping serializable data to a React component in an application
|
||||
- Editing data using a form generated from a JSON schema
|
||||
- Viewing a React component in an isolated iframe environment
|
||||
- Using a navigation generated from a components data
|
||||
- All of the above to create a live editing UI
|
||||
|
||||
## Concepts
|
||||
|
||||
### Ecosystem
|
||||
|
||||
The following components are intended to work together as an ecosystem of components:
|
||||
|
||||
- `ModularForm` - see [Form](#form)
|
||||
- `ModularViewer` - see [Viewer](#viewer)
|
||||
- `ModularNavigation` - see [Navigation](#navigation)
|
||||
|
||||
Each of these components is provided as a standalone version and a version intended to work with another of the above components. If the `Form` is intended to be used with the `Viewer` then the `Modular` prefixed versions should be used. This enables them to share certain capabilities such as drag and drop.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import { DndProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import { ModularForm, ModularViewer } from "@microsoft/fast-tooling-react";
|
||||
|
||||
// See details on implementation from the standalone
|
||||
// versions of Form and Viewer
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ModularForm {...props} />
|
||||
<ModularViewer {...props} />
|
||||
</DndProvider>
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
`npm i --save @microsoft/fast-tooling-react`
|
||||
|
||||
## Requirements
|
||||
|
||||
The `@microsoft/fast-tooling-react` package will be installed with `@microsoft/fast-tooling`. The `@microsoft/fast-tooling` package includes exports required for implementing the React specific components, namely the `MessageSystem` and the minified webworker which handles data manipulation. Please refer to the documentation for `@microsoft/fast-tooling` for a better understanding of these systems.
|
||||
|
||||
## Form
|
||||
|
||||
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up.
|
||||
|
||||
Example:
|
||||
```jsx
|
||||
import { Form } from "@microsoft/fast-tooling-react";
|
||||
|
||||
/**
|
||||
* Add to your render function
|
||||
*/
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
/>
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Validation is treated as optional, there is a validation utility provided by the `@microsoft/fast-tooling` package that will give basic JSON schema validation errors. Refer to the `@microsoft/fast-tooling` README for details.
|
||||
|
||||
### Drag and drop
|
||||
|
||||
Drag and drop is provided to the `Form` using the `react-dnd` package as well as the `HTML5Backend`. If you are using `react-dnd` somewhere else and need to implement the backend once, use the secondary export `ModularForm`.
|
||||
|
||||
### Using form control plugins
|
||||
|
||||
All necessary form controls are built in by default but can be overriden either through the schema by adding a `formControlId` property with a string value or a control type defined [below](#list-of-control-types).
|
||||
|
||||
To make a custom control, use the secondary export `StandardControlPlugin` which will take care of all standard form actions such as setting default, resetting data, etc. You will need to provide the necessary functionality to the `control` as JSX.
|
||||
|
||||
When the plugin instance is passed to the `<Form />`
|
||||
either the id or the type is then referenced and will cause the control to render.
|
||||
|
||||
A config is passed to the control, the specifications of this can be found [here](https://github.com/microsoft/fast/blob/master/packages/fast-tooling-react/src/form/templates/template.control.utilities.props.tsx). Note that the `ControlConfig` interface may include extra properties depending on the control type being used.
|
||||
|
||||
Example id plugin:
|
||||
|
||||
JSON Schema:
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "string",
|
||||
"formControlId": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
JSX:
|
||||
```jsx
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
controls={[
|
||||
new StandardControlPlugin({
|
||||
id: "foo",
|
||||
control: (config) => {
|
||||
return (
|
||||
<input
|
||||
value={config.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
Example type plugin:
|
||||
|
||||
```jsx
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
controls={[
|
||||
new StandardControlPlugin({
|
||||
type: ControlType.textarea,
|
||||
control: (config) => {
|
||||
return (
|
||||
<input
|
||||
value={config.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
]}
|
||||
/>
|
||||
|
||||
```
|
||||
|
||||
#### List of control types
|
||||
|
||||
Control types are available as an enum provided as a secondary export `ControlType` and consist of the following:
|
||||
|
||||
```js
|
||||
import { ControlType } from "@microsoft/fast-tooling-react";
|
||||
|
||||
// Available types
|
||||
ControlType.select
|
||||
ControlType.array
|
||||
ControlType.checkbox
|
||||
ControlType.linkedData
|
||||
ControlType.numberField
|
||||
ControlType.sectionLink
|
||||
ControlType.section
|
||||
ControlType.display
|
||||
ControlType.button
|
||||
ControlType.textarea
|
||||
```
|
||||
|
||||
These control types can be paired with our default controls, the following of which are available:
|
||||
|
||||
- `SelectControl`
|
||||
- `ArrayControl`
|
||||
- `CheckboxControl`
|
||||
- `LinkedDataControl`
|
||||
- `NumberFieldControl`
|
||||
- `SectionLinkControl`
|
||||
- `SectionControl`
|
||||
- `DisplayControl`
|
||||
- `ButtonControl`
|
||||
- `TextareaControl`
|
||||
|
||||
**Note: If the id and type are not specified, all controls will be replaced with the control.**
|
||||
|
||||
Example of a replacement type:
|
||||
|
||||
```jsx
|
||||
import { ControlType, TextareaControl } from "@microsoft/fast-tooling-react";
|
||||
|
||||
...
|
||||
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
controls={[
|
||||
new StandardControlPlugin({
|
||||
type: ControlType.textarea,
|
||||
control: (config) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>Hello world!</span>
|
||||
<TextareaControl {...config} />
|
||||
</React.Fragement>
|
||||
);
|
||||
}
|
||||
})
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
Example of a replacement for all controls, using the component for the default control:
|
||||
|
||||
```jsx
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
controls={[
|
||||
new StandardControlPlugin({
|
||||
control: (config) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span>Hello world!</span>
|
||||
<config.component {...config} />
|
||||
</React.Fragement>
|
||||
);
|
||||
}
|
||||
})
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Making your own control plugin
|
||||
|
||||
The `StandardControlPlugin` creates a standard template for expected functionality not specific to a control such as a `CheckboxControl`. This may include showing a button to set the value to the default value, an unset/reset button if the value represented in the control is optional, etc.
|
||||
|
||||
It is possible to create your own control plugin template; this section is for more advanced usage and should be done with caution.
|
||||
|
||||
To assist in the creation of a custom control plugin template, another secondary export is provided, `ControlTemplateUtilities`. This is an abstract class that can be extended, it includes all of the render methods for various actions that can be taken that are not control specific. It is possible to use this class to make your own template and include extra logic for when these items should render.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import { ControlTemplateUtilities } from "@microsoft/fast-tooling-react";
|
||||
|
||||
export class MyControlTemplate extends ControlTemplateUtilities {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<label
|
||||
htmlFor={this.props.dataLocation}
|
||||
title={this.props.labelTooltip}
|
||||
>
|
||||
{this.props.label}
|
||||
</label>
|
||||
{this.renderConstValueIndicator("const-value-indicator-css-class")}
|
||||
{this.renderDefaultValueIndicator("default-value-indicator-css-class")}
|
||||
{this.renderBadge("badge-css-class")}
|
||||
{this.renderControl(this.props.control(this.getConfig()))}
|
||||
{this.renderSoftRemove("soft-remove-css-class")}
|
||||
{this.renderInvalidMessage("invalid-message-css-class")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { StandardControlTemplate };
|
||||
```
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `renderConstValueIndicator` - This will indicate that this value is not the const value and will set the value to the const value if clicked.
|
||||
- `renderDefaultValueIndicator` - This will indicate that this value is not the default value and will set the value to the default if clicked.
|
||||
- `renderBadge` - This renders a badge, as indicated by the [badge](#badges) section of this README.
|
||||
- `renderControl` - This renders the control, such as `CheckboxControl` etc. or whatever control has been specified if the default controls are not being used. This must include an argument to execute the control with the `getConfig` method as an argument.
|
||||
- `renderSoftRemove` - This allows for the rendering of an unset/reset button if the value of this control is optional.
|
||||
- `renderInvalidMessage` - This method renders the invalid message for this control as specified when validating the data against the JSON schema.
|
||||
|
||||
Note that with the exception of `renderControl` method, the others require a string argument, this will be used as a class so that the generated HTML from the render method can be styled. At this point it is up to the implementer to include their own styling for these items.
|
||||
|
||||
It is recommended that the implementation also include the use of a label for accessibility.
|
||||
|
||||
### Validation
|
||||
|
||||
Form validation uses the [ajv](https://github.com/epoberezkin/ajv) package. The validation can be displayed inline or using the browser default HTML5 validation UI. This can be achieved through the `displayValidationBrowserDefault` which is `true` by default and `displayValidationInline` which will show validation messages below the associated form element.
|
||||
|
||||
### JSON schema metadata
|
||||
|
||||
The schema form generator can interpret most [JSON schemas](http://json-schema.org/), however there are some things to note when writing JSON schemas that make for a better UI.
|
||||
|
||||
#### Title
|
||||
|
||||
Using a title will add a label to the corresponding form element. All properties are required to have a title.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string",
|
||||
"example": "Hello world"
|
||||
},
|
||||
"weight": {
|
||||
"title": "Weight",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"heavy"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Description
|
||||
|
||||
Using a description will add a HTML attribute `title` to the label, resulting in a browser tooltip. This should be used for supplemental information that may not be apparent in the title.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"description": "The text appearing in the body",
|
||||
"type": "string",
|
||||
"example": "Hello world"
|
||||
},
|
||||
"weight": {
|
||||
"title": "Weight",
|
||||
"description": "The weight of the text",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"heavy"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Disabled
|
||||
|
||||
The disabled flag is optional and the form item representing this section of the schema will be disabled if flag is set to true.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string",
|
||||
"example": "Hello world",
|
||||
"disabled": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples & default
|
||||
|
||||
Providing an examples or default value will replace the placeholder 'example text' or randomly generated number. It is generally better to add this extra information in case the schema form generator needs to create a new set of data.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"Hello world"
|
||||
]
|
||||
},
|
||||
"style": {
|
||||
"title": "Style",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"title": "HEX Color",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"#FF0000"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"color"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Because the style is optional, you can toggle to add it. The schema form generator will see that color is a required piece of data and use the example given to fill in.
|
||||
|
||||
#### Badges
|
||||
|
||||
To allow more detail about a field two additional fields can be added to JSON schemas, `badge` and `badgeDescription`. The `badge` can have the values "info", "warning" and "locked" which will create the related icons. Adding a `badgeDescription` will add a native tooltip when the badge is hovered.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "string",
|
||||
"badge": "warning",
|
||||
"badgeDescription": "Setting this field will cause adverse effects"
|
||||
}
|
||||
```
|
||||
|
||||
#### Alias
|
||||
|
||||
Occasionally the `title` provided by the JSON schema may not be enough information for the `Form` component, if an additional `alias` property is provided, this will be used as the linked data control label. In Chromium based browsers this will show both, and both the `title` text and `alias` text will autocomplete. In Firefox however, this will result in only showing the `alias`, so ensure that the `alias` text contains enough information to be easily autocompleted by a user.
|
||||
|
||||
#### Dictionaries
|
||||
|
||||
The `additionalProperties` JSON schema keyword can be used to create a dictionary of user-input keys on an object. To give these keys a label add the keyword `propertyTitle`, this will create a label for the form element for editing the property key.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"title": "A dictionary of strings",
|
||||
"propertyTitle": "A dictionary key",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Categories
|
||||
|
||||
For improved UI when there become too many properties in an object, categories can be specified on a per-schema basis.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
categories={{
|
||||
"category-schema-id": {
|
||||
"": [
|
||||
{
|
||||
title: "Style",
|
||||
dataLocations: ["color", "outline", "font"],
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
dataLocations: ["title", "body", "footer"],
|
||||
},
|
||||
{
|
||||
title: "Advanced",
|
||||
dataLocations: ["tracking", "accessibility"],
|
||||
expandByDefault: false // default true
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
This shows the root object of a schema with `$id` of `category-schema-id` that has the properties `color`, `outline`, `font`, `title`, `body`, `footer`, `tracking` and `accessibility` and splits them into categories with the appropriate titles.
|
||||
|
||||
The "Advanced" category has its expand default set to `false`, this means that initially it will be collapsed.
|
||||
|
||||
### JSON schema keywords
|
||||
|
||||
Certain JSON schema keywords are interpreted to provide a better UI.
|
||||
|
||||
#### oneOf & anyOf
|
||||
|
||||
The oneOf and anyOf keywords can be used inside a property and at the root level of a schema. This will create a select dropdown so that the user can switch between them. If data has been provided, it will select the first oneOf/anyOf instance it can validate against. The contents of a 'title' property will be used for the contents of the dropdown.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"oneOf": [
|
||||
{
|
||||
"title": "color",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"title": "HEX Color",
|
||||
"type": "string",
|
||||
"example": "#FF0000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "text",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"title": "Text",
|
||||
"type": "string",
|
||||
"example": "Hello world"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Enums
|
||||
|
||||
Any enums will be converted to a select dropdown.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/schema#",
|
||||
"id": "my-component",
|
||||
"title": "My component",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"title": "Color",
|
||||
"type": "string",
|
||||
"enum" : [
|
||||
"red",
|
||||
"green",
|
||||
"blue",
|
||||
"yellow"
|
||||
],
|
||||
"default": "red"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### allOf & $ref
|
||||
|
||||
The `allOf` and `$ref` keywords cannot be interpreted by the schema form generator.
|
||||
|
||||
#### Categories
|
||||
|
||||
Any `object` in the `<Form />` may have categories with which to contain its properties. This can be achieved by passing the `categories` prop which is a dictionary of keys that match to a schemas `id`, and which contain a `dataLocation` key to indicate which object a form category belongs to. Each category can then specify the properties as a set of `dataLocation` strings and a `title`.
|
||||
|
||||
Example:
|
||||
|
||||
```tsx
|
||||
<Form
|
||||
messageSystem={fastMessageSystem}
|
||||
categories={{
|
||||
"https://my.schema.id": {
|
||||
"": [ // The root level dataLocation
|
||||
{
|
||||
title: "Style",
|
||||
dataLocations: ["border", "font"]
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
dataLocations: ["text", "title"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Navigation
|
||||
messageSystem={fastMessageSystem}
|
||||
/>
|
||||
```
|
||||
|
||||
### Include data types
|
||||
|
||||
By default all JSON schema data types are visible. If the optional `types` prop is included, the data types that are in the array will become the data types that are rendered. If a data type that is specified is contained inside a data type that is not, that will directly nest the visible data type with the parent of the unspecified data type.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Navigation
|
||||
messageSystem={fastMessageSystem}
|
||||
types={["object", "string"]} // only "object" and "string" data types are rendered
|
||||
/>
|
||||
```
|
||||
|
||||
## Navigation Menu
|
||||
|
||||
The `NavigationMenu` component creates a navigational menu from a provided data structure. This component is meant to serve as location routing in an application.
|
||||
|
||||
### Menu structure
|
||||
Example menu structure:
|
||||
```js
|
||||
const menu = [
|
||||
{
|
||||
displayName: "red",
|
||||
location: "/red"
|
||||
},
|
||||
{
|
||||
displayName: "green",
|
||||
items: [
|
||||
{
|
||||
displayName: "blue",
|
||||
location: "/blue"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Each menu item requires a `displayName` to use in the generated UI.
|
||||
|
||||
Simple example:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
return (
|
||||
<NavigationMenu
|
||||
menu={menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling the location
|
||||
|
||||
A `location` may optionally be provided in the menu data, if this is not accompanied by a `onLocationUpdate` callback, the generated UI for that menu item will be an anchor. If an `onLocationUpdate` callback is provided this results in a span, which when clicked will fire the callback with the associated location.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
render() {
|
||||
return (
|
||||
<NavigationMenu
|
||||
menu={menu}
|
||||
activeLocation={this.state.activeLocation}
|
||||
onLocationUpdate={this.handleLocationUpdate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleLocationUpdate = (location) => {
|
||||
this.setState({
|
||||
activeLocation: location
|
||||
});
|
||||
|
||||
// do some route manipulation
|
||||
}
|
||||
```
|
||||
|
||||
## Viewer
|
||||
|
||||
The `Viewer` component creates an iframe, it can have a fixed or adjustable width and height and can be used independently or as a set with the `SelectDevice` and `Rotate` components.
|
||||
|
||||
The required property is the `messageSystem`, see `@microsoft/fast-tooling` for details on setting this up and the `iframeSrc`.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Viewer
|
||||
messageSystem={fastMessageSystem}
|
||||
iframeSrc={"/example-content"}
|
||||
/>
|
||||
```
|
||||
|
||||
### Setting width and height
|
||||
|
||||
The `width` and `height` can be set on the `Viewer` component which will be used as pixel values:
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Viewer
|
||||
messageSystem={fastMessageSystem}
|
||||
iframeSrc={"/example-content"}
|
||||
width={500}
|
||||
height={300}
|
||||
/>
|
||||
```
|
||||
|
||||
To create a responsive width an height, the `width` and `height` can be tied to values in state and combined with the `onUpdateHeight`, `onUpdateWidth` and `responsive` props. This creates draggable borders around the iframe.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<Viewer
|
||||
messageSystem={fastMessageSystem}
|
||||
iframeSrc={"/example-content"}
|
||||
width={this.state.viewerWidth}
|
||||
height={this.state.viewerHeight}
|
||||
responsive={true}
|
||||
onUpdateHeight={this.handleUpdateViewerHeight}
|
||||
onUpdateWidth={this.handleUpdateViewerWidth}
|
||||
/>
|
||||
|
||||
// handlers for the `onUpdateHeight` and `onUpdateWidth` callbacks
|
||||
handleUpdateViewerHeight = (newViewerHeight) => {
|
||||
this.setState({
|
||||
viewerHeight: newViewerHeight
|
||||
});
|
||||
}
|
||||
|
||||
handleUpdateViewerWidth = (newViewerWidth) => {
|
||||
this.setState({
|
||||
viewerWidth: newViewerWidth
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Sending custom messages
|
||||
|
||||
Sending custom messages through from the iframe can be done with a `postMessage` to the iframe window. The custom message should define the `type` and `action`. The type should be `MessageSystemType.custom` imported from the `@microsoft/fast-tooling` package and the `action` is defined as the enum value `ViewerCustomAction.call` provided as an export.
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
import { MessageSystemType } from "@microsoft/fast-tooling";
|
||||
import { ViewerCustomAction } from "@microsoft/fast-tooling-react";
|
||||
|
||||
window.postMessage({
|
||||
type: MessageSystemType.custom,
|
||||
action: ViewerCustomAction.call,
|
||||
data: myData
|
||||
}, "*");
|
||||
```
|
||||
|
||||
### Receiving custom messages
|
||||
|
||||
When a custom message is sent through the message system with a type of `ViewerCustomAction.call`, it will be passed to all registered callbacks with the message system using a modified `action` type of `ViewerCustomAction.response`. This way any further action that needs to be taken with the message data passed from the iframe can be done by looking for the response.
|
||||
|
||||
### Select device
|
||||
|
||||
Use the `SelectDevice` component to select from provided default devices or provide your own custom device configurations. This component accepts a list of configured devices via the `devices` prop, some default devices are included with the package as a secondary export. It also accepts an `activeDeviceId` prop which maps to the current device id of the provided devices. In addition there is a callback `onUpdateDevice` which will fire a provided function with the new device id selected.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import {
|
||||
defaultDevices,
|
||||
SelectDevice,
|
||||
} from "@microsoft/fast-tooling-react";
|
||||
|
||||
<SelectDevice
|
||||
devices={defaultDevices}
|
||||
onUpdateDevice={this.handleDeviceUpdate}
|
||||
activeDeviceId={this.state.activeDevice.id}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Devices
|
||||
|
||||
A device can be either "responsive" or "fixed", if it is responsive it does not take a width and height. The current active device can be used to activate the `responsive` prop on the `Viewer` component.
|
||||
|
||||
Example of custom devices passed to the `devices` prop:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "responsive",
|
||||
"displayName": "Responsive display",
|
||||
"display": "responsive"
|
||||
},
|
||||
{
|
||||
"id": "phoneDevice",
|
||||
"displayName": "Phone device",
|
||||
"display": "fixed",
|
||||
"height": 800,
|
||||
"width": 320
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Rotate
|
||||
|
||||
Use the `Rotate` component to switch between landscape and portrait view. This component accepts an `orientation` prop which can be either "landscape" or "portrait". It also accepts an `onUpdateOrientation` callback which will fire a provided function with the new orientation selected.
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
import {
|
||||
Rotate,
|
||||
} from "@microsoft/fast-tooling-react";
|
||||
|
||||
<Rotate
|
||||
orientation={this.state.orientation}
|
||||
onUpdateOrientation={this.handleOrientationUpdate}
|
||||
/>
|
||||
```
|
||||
|
||||
## Data utilities
|
||||
|
||||
### Transforming data
|
||||
|
||||
As data from the dictionary of data is intended to be mapped to JSON schema, it may need to be transformed to be useful as, for instance, a React component.
|
||||
|
||||
Assuming that each JSON schema represents React props for a given component, a mapper has been provided which can be used in conjunction with the `@microsoft/fast-tooling` export `mapDataDictionary`.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
import { mapDataDictionary } from "@microsoft/fast-tooling";
|
||||
import { reactMapper } from "@microsoft/fast-tooling-react";
|
||||
|
||||
const componentDictionary = {
|
||||
"button-schema-id": MyButton
|
||||
}
|
||||
|
||||
const myComponentInstance = mapDataDictionary({
|
||||
dataDictionary: {
|
||||
foo: {
|
||||
schemaId: "button-schema-id",
|
||||
data: {
|
||||
children: "Hello world",
|
||||
},
|
||||
},
|
||||
},
|
||||
dataDictionaryKey: "foo",
|
||||
mapper: reactMapper(componentDictionary),
|
||||
schemaDictionary: {
|
||||
"button-schema-id": {
|
||||
id: "button-schema-id",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Expected result from the example above is an instance of `MyButton` with the text "Hello world". This is a simple mapper that assumes that any linked data is a React component that is nested.
|
|
@ -0,0 +1,3 @@
|
|||
# Background
|
||||
|
||||
This folder contains a React application used for testing. None of the files in this folder are intended for export, it should consume exports from the `./src` folder to create a variety of experiences and to validate the components working with each other.
|
|
@ -0,0 +1,91 @@
|
|||
import React from "react";
|
||||
import { BrowserRouter, Link, Redirect, Route, Switch } from "react-router-dom";
|
||||
import { NavigationTestPage } from "./pages/navigation";
|
||||
import ViewerPage from "./pages/viewer";
|
||||
import ViewerContentPage from "./pages/viewer/content";
|
||||
import { FormTestPage } from "./pages/form";
|
||||
import { FormAndNavigationTestPage } from "./pages/form-and-navigation";
|
||||
import { NavigationMenuTestPage } from "./pages/navigation-menu";
|
||||
import { WebComponentTestPage } from "./pages/web-components";
|
||||
import WebComponentViewerContent from "./pages/web-components/web-component-viewer-content";
|
||||
|
||||
class App extends React.Component<{}, {}> {
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
{this.renderLinks()}
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/navigation-menu"}
|
||||
component={NavigationMenuTestPage}
|
||||
/>
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/navigation"}
|
||||
component={NavigationTestPage}
|
||||
/>
|
||||
<Route exact={true} path={"/viewer"} component={ViewerPage} />
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/viewer/content"}
|
||||
component={ViewerContentPage}
|
||||
/>
|
||||
<Route exact={true} path={"/form"} component={FormTestPage} />
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/form-and-navigation"}
|
||||
component={FormAndNavigationTestPage}
|
||||
/>
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/web-components"}
|
||||
component={WebComponentTestPage}
|
||||
/>
|
||||
<Route
|
||||
exact={true}
|
||||
path={"/web-components/content"}
|
||||
component={WebComponentViewerContent}
|
||||
/>
|
||||
<Route exact={true} path={"/"}>
|
||||
<Redirect to={"/form"} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLinks(): React.ReactNode {
|
||||
if (window.location.pathname.slice(-7) !== "content") {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/navigation-menu">Navigation Menu</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/form">Form</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/navigation">Navigation</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/form-and-navigation">Form and Navigation</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/viewer">Viewer</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/web-components">Web Components</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./app";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import { DndProvider } from "react-dnd";
|
||||
|
||||
/**
|
||||
* Create the root node
|
||||
*/
|
||||
const root: HTMLElement = document.createElement("div");
|
||||
root.setAttribute("id", "root");
|
||||
document.body.appendChild(root);
|
||||
document.body.setAttribute("style", "margin: 0");
|
||||
|
||||
/**
|
||||
* Primary render function for app. Called on store updates
|
||||
*/
|
||||
ReactDOM.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<App />
|
||||
</DndProvider>,
|
||||
document.getElementById("root")
|
||||
);
|
|
@ -0,0 +1,311 @@
|
|||
import React from "react";
|
||||
import { DesignSystemProvider } from "@microsoft/fast-jss-manager-react";
|
||||
import { ModularForm, ModularNavigation } from "../../src";
|
||||
import { FormProps } from "../../src/form/form.props";
|
||||
import {
|
||||
FormAttributeSettingsMappingToPropertyNames,
|
||||
FormChildOptionItem,
|
||||
} from "../../src/form/types";
|
||||
import * as testConfigs from "./form/";
|
||||
import {
|
||||
DataDictionary,
|
||||
getDataFromSchema,
|
||||
MessageSystem,
|
||||
MessageSystemType,
|
||||
SchemaDictionary,
|
||||
} from "@microsoft/fast-tooling";
|
||||
|
||||
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
|
||||
|
||||
export interface FormTestPageState {
|
||||
schema: any;
|
||||
data: any;
|
||||
dataDictionary: DataDictionary<unknown>;
|
||||
navigation: any;
|
||||
attributeAssignment?: FormAttributeSettingsMappingToPropertyNames;
|
||||
showExtendedControls: boolean;
|
||||
dataLocation: string;
|
||||
defaultBrowserErrors?: boolean;
|
||||
inlineErrors?: boolean;
|
||||
}
|
||||
|
||||
export interface GroupItem {
|
||||
items: any;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const designSystemDefaults: any = {
|
||||
foregroundColor: "#000",
|
||||
backgroundColor: "#FFF",
|
||||
brandColor: "#0078D4",
|
||||
};
|
||||
|
||||
let fastMessageSystem: MessageSystem;
|
||||
|
||||
class FormAndNavigationTestPage extends React.Component<{}, FormTestPageState> {
|
||||
/**
|
||||
* These are the children that can be added
|
||||
*/
|
||||
private childOptions: FormChildOptionItem[];
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.childOptions = this.getChildOptions();
|
||||
|
||||
const exampleData: any = getDataFromSchema(testConfigs.textField.schema);
|
||||
|
||||
if ((window as any).Worker) {
|
||||
fastMessageSystem = new MessageSystem({
|
||||
webWorker: "message-system.js",
|
||||
dataDictionary: [
|
||||
{
|
||||
"": {
|
||||
schemaId: testConfigs.textField.schema.id,
|
||||
data: exampleData,
|
||||
},
|
||||
},
|
||||
"",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
|
||||
}
|
||||
|
||||
this.state = {
|
||||
schema: testConfigs.textField.schema,
|
||||
data: exampleData,
|
||||
dataDictionary: [
|
||||
{
|
||||
"": {
|
||||
schemaId: testConfigs.textField.schema.id,
|
||||
data: exampleData,
|
||||
},
|
||||
},
|
||||
"",
|
||||
],
|
||||
navigation: void 0,
|
||||
showExtendedControls: false,
|
||||
dataLocation: "",
|
||||
inlineErrors: void 0,
|
||||
defaultBrowserErrors: void 0,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<DesignSystemProvider designSystem={designSystemDefaults}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: "300px",
|
||||
height: "100vh",
|
||||
float: "left",
|
||||
fontFamily:
|
||||
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
{this.renderNavigation()}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
float: "left",
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<select onChange={this.handleComponentUpdate}>
|
||||
{this.getComponentOptions()}
|
||||
</select>
|
||||
<br />
|
||||
<br />
|
||||
<input
|
||||
id={"showInlineErrors"}
|
||||
type="checkbox"
|
||||
value={(!!this.state.inlineErrors).toString()}
|
||||
onChange={this.handleShowInlineErrors}
|
||||
/>
|
||||
<label htmlFor={"showInlineErrors"}>Show inline errors</label>
|
||||
<br />
|
||||
<input
|
||||
id={"showBrowserErrors"}
|
||||
type="checkbox"
|
||||
value={(!!this.state.defaultBrowserErrors).toString()}
|
||||
onChange={this.handleShowBrowserErrors}
|
||||
/>
|
||||
<label htmlFor={"showBrowserErrors"}>
|
||||
Show default browser errors
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgb(244, 245, 246)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(this.state.dataDictionary, null, 2)}
|
||||
</pre>
|
||||
<pre>{this.state.dataLocation}</pre>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "300px",
|
||||
height: "100vh",
|
||||
float: "left",
|
||||
fontFamily:
|
||||
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
<ModularForm {...this.coerceFormProps()} />
|
||||
</div>
|
||||
</div>
|
||||
</DesignSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private renderNavigation(): React.ReactNode {
|
||||
return <ModularNavigation messageSystem={fastMessageSystem} />;
|
||||
}
|
||||
|
||||
private generateSchemaDictionary(): SchemaDictionary {
|
||||
const schemaDictionary: SchemaDictionary = {};
|
||||
|
||||
Object.keys(testConfigs).forEach((testConfigKey: string) => {
|
||||
schemaDictionary[testConfigs[testConfigKey].schema.id] =
|
||||
testConfigs[testConfigKey].schema;
|
||||
});
|
||||
|
||||
return schemaDictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child options for the schema form
|
||||
*/
|
||||
private getChildOptions(): FormChildOptionItem[] {
|
||||
const childOptions: FormChildOptionItem[] = [];
|
||||
const groups: GroupItem[] = [
|
||||
{
|
||||
items: testConfigs,
|
||||
type: "components",
|
||||
},
|
||||
];
|
||||
|
||||
for (const group of groups) {
|
||||
Object.keys(group.items).map((itemName: any, key: number): void => {
|
||||
if (typeof testConfigs[itemName].schema !== "undefined") {
|
||||
const childObj: FormChildOptionItem = {
|
||||
name: testConfigs[itemName].schema.title || "Untitled",
|
||||
component: testConfigs[itemName].component,
|
||||
schema: testConfigs[itemName].schema,
|
||||
};
|
||||
|
||||
childOptions.push(childObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return childOptions;
|
||||
}
|
||||
|
||||
private coerceFormProps(): FormProps {
|
||||
const formProps: FormProps = {
|
||||
messageSystem: fastMessageSystem,
|
||||
};
|
||||
|
||||
if (typeof this.state.defaultBrowserErrors === "boolean") {
|
||||
formProps.displayValidationBrowserDefault = this.state.defaultBrowserErrors;
|
||||
}
|
||||
|
||||
if (typeof this.state.inlineErrors === "boolean") {
|
||||
formProps.displayValidationInline = this.state.inlineErrors;
|
||||
}
|
||||
|
||||
return formProps;
|
||||
}
|
||||
|
||||
private handleShowInlineErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (e.target.value === "true") {
|
||||
this.setState({
|
||||
inlineErrors: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
inlineErrors: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleShowBrowserErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (e.target.value === "true") {
|
||||
this.setState({
|
||||
defaultBrowserErrors: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
defaultBrowserErrors: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleMessageSystem = (e: MessageEvent): void => {
|
||||
switch (e.data.type) {
|
||||
case MessageSystemType.initialize:
|
||||
if (e.data.data && e.data.navigation) {
|
||||
this.setState({
|
||||
data: e.data.data,
|
||||
dataDictionary: e.data.dataDictionary,
|
||||
navigation: e.data.navigation,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessageSystemType.data:
|
||||
if (e.data.data) {
|
||||
this.setState({
|
||||
data: e.data.data,
|
||||
dataDictionary: e.data.dataDictionary,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessageSystemType.navigation:
|
||||
if (e.data.navigation) {
|
||||
this.setState({
|
||||
data: e.data.navigation,
|
||||
dataDictionary: e.data.dataDictionary,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
const data: any = !!testConfigs[e.target.value].data
|
||||
? testConfigs[e.target.value].data
|
||||
: getDataFromSchema(testConfigs[e.target.value].schema);
|
||||
|
||||
if ((window as any).Worker && fastMessageSystem) {
|
||||
fastMessageSystem.postMessage({
|
||||
type: MessageSystemType.initialize,
|
||||
data: [
|
||||
{
|
||||
"": {
|
||||
schemaId: testConfigs[e.target.value].schema.id,
|
||||
data,
|
||||
},
|
||||
},
|
||||
"",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private getComponentOptions(): JSX.Element[] {
|
||||
return Object.keys(testConfigs).map((testComponentKey: any, index: number) => {
|
||||
return <option key={index}>{testConfigs[testComponentKey].schema.id}</option>;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { FormAndNavigationTestPage };
|
|
@ -0,0 +1,585 @@
|
|||
import * as testConfigs from "./form/";
|
||||
import { AlignControl, Form } from "../../src";
|
||||
import {
|
||||
ControlConfig,
|
||||
StandardControlPlugin,
|
||||
TextAlignControl,
|
||||
FileControl,
|
||||
} from "../../src";
|
||||
import CSSControl from "../../src/form/custom-controls/control.css";
|
||||
import { properties as allCSSProperties } from "@microsoft/fast-tooling/dist/esm/css-data";
|
||||
import { FormProps } from "../../src/form/form.props";
|
||||
import {
|
||||
FormAttributeSettingsMappingToPropertyNames,
|
||||
FormChildOptionItem,
|
||||
} from "../../src/form/types";
|
||||
import React from "react";
|
||||
import {
|
||||
AjvMapper,
|
||||
DataDictionary,
|
||||
getDataFromSchema,
|
||||
MessageSystem,
|
||||
MessageSystemType,
|
||||
SchemaDictionary,
|
||||
} from "@microsoft/fast-tooling";
|
||||
import {
|
||||
accentColorName,
|
||||
L1ColorName,
|
||||
L4ColorName,
|
||||
textColorName,
|
||||
L3FillColorName,
|
||||
errorColorName,
|
||||
FloatingColorName,
|
||||
} from "../../src/style";
|
||||
import { CSSPropertiesDictionary } from "@microsoft/fast-tooling/dist/esm/data-utilities/mapping.mdn-data";
|
||||
import { ControlContext } from "../../src/form/templates/types";
|
||||
import { CSSStandardControlPlugin } from "../../src/form/custom-controls/css";
|
||||
import { CSSControlConfig } from "../../src/form/custom-controls/css/css.template.control.standard.props";
|
||||
import { DesignSystem } from "@microsoft/fast-foundation";
|
||||
import {
|
||||
fastButton,
|
||||
fastCheckbox,
|
||||
fastNumberField,
|
||||
fastOption,
|
||||
fastSelect,
|
||||
fastTextField,
|
||||
} from "@microsoft/fast-components";
|
||||
import {
|
||||
fastToolingColorPicker,
|
||||
fastToolingFile,
|
||||
fastToolingFileActionObjectUrl,
|
||||
} from "@microsoft/fast-tooling/dist/esm/web-components";
|
||||
|
||||
DesignSystem.getOrCreate().register(
|
||||
fastButton(),
|
||||
fastCheckbox(),
|
||||
fastNumberField(),
|
||||
fastOption(),
|
||||
fastSelect(),
|
||||
fastTextField(),
|
||||
fastToolingColorPicker({ prefix: "fast-tooling" }),
|
||||
fastToolingFile({ prefix: "fast-tooling" }),
|
||||
fastToolingFileActionObjectUrl({ prefix: "fast-tooling" })
|
||||
);
|
||||
|
||||
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
|
||||
|
||||
export interface FormTestPageState {
|
||||
schema: any;
|
||||
data: any;
|
||||
dataDictionary: DataDictionary<unknown>;
|
||||
navigation: any;
|
||||
attributeAssignment?: FormAttributeSettingsMappingToPropertyNames;
|
||||
showExtendedControls: boolean;
|
||||
defaultBrowserErrors?: boolean;
|
||||
inlineErrors?: boolean;
|
||||
dataSet?: any;
|
||||
cssPropertyOverrides: boolean;
|
||||
}
|
||||
|
||||
export interface GroupItem {
|
||||
items: any;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface DataSet {
|
||||
displayName: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
const properties = {
|
||||
"border-width": allCSSProperties["border-width"],
|
||||
"border-style": allCSSProperties["border-style"],
|
||||
"border-color": allCSSProperties["border-color"],
|
||||
"outline-offset": allCSSProperties["outline-offset"],
|
||||
"animation-delay": allCSSProperties["animation-delay"],
|
||||
};
|
||||
|
||||
const dataSets: DataSet[] = [
|
||||
{
|
||||
displayName: "Data set 1 (all defined)",
|
||||
data: {
|
||||
textarea: "alpha",
|
||||
"section-link": {},
|
||||
checkbox: true,
|
||||
button: null,
|
||||
array: ["foo", "bar"],
|
||||
"number-field": 42,
|
||||
select: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: "Data set 2 (select defined)",
|
||||
data: {
|
||||
textarea: "beta",
|
||||
"section-link": {},
|
||||
display: "foobar",
|
||||
checkbox: false,
|
||||
"number-field": 24,
|
||||
select: "bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: "Data set 3 (none defined)",
|
||||
data: {},
|
||||
},
|
||||
];
|
||||
|
||||
const CSSpropertyOverrides = {
|
||||
[accentColorName]: "blue",
|
||||
[L1ColorName]: "white",
|
||||
[L4ColorName]: "lightgray",
|
||||
[textColorName]: "black",
|
||||
[L3FillColorName]: "white",
|
||||
[errorColorName]: "green",
|
||||
[FloatingColorName]: "purple",
|
||||
};
|
||||
|
||||
let fastMessageSystem: MessageSystem;
|
||||
let ajvMapper: AjvMapper;
|
||||
|
||||
class FormTestPage extends React.Component<{}, FormTestPageState> {
|
||||
/**
|
||||
* These are the children that can be added
|
||||
*/
|
||||
private childOptions: FormChildOptionItem[];
|
||||
|
||||
/**
|
||||
* The custom control plugins used in the form
|
||||
*/
|
||||
private controlPlugins: StandardControlPlugin[];
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.childOptions = this.getChildOptions();
|
||||
|
||||
this.controlPlugins = [
|
||||
new StandardControlPlugin({
|
||||
id: testConfigs.customControl.schema.properties.file.formControlId,
|
||||
control: (config: ControlConfig): React.ReactNode => {
|
||||
return (
|
||||
<FileControl {...config} accept=".jpg,.jpeg,.png,.gif">
|
||||
Add Image
|
||||
</FileControl>
|
||||
);
|
||||
},
|
||||
}),
|
||||
new StandardControlPlugin({
|
||||
id: testConfigs.customControl.schema.properties.textAlign.formControlId,
|
||||
control: (config: ControlConfig): React.ReactNode => {
|
||||
return <TextAlignControl {...config} />;
|
||||
},
|
||||
}),
|
||||
new StandardControlPlugin({
|
||||
id: testConfigs.customControl.schema.properties.align.formControlId,
|
||||
control: (config: ControlConfig): React.ReactNode => {
|
||||
return <AlignControl {...config} />;
|
||||
},
|
||||
}),
|
||||
new StandardControlPlugin({
|
||||
id: testConfigs.controlPluginCss.schema.properties.css.formControlId,
|
||||
context: ControlContext.fill,
|
||||
control: (config: ControlConfig): React.ReactNode => {
|
||||
return (
|
||||
<CSSControl
|
||||
css={(properties as unknown) as CSSPropertiesDictionary}
|
||||
{...config}
|
||||
key={`${config.dictionaryId}::${config.dataLocation}`}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
new StandardControlPlugin({
|
||||
id:
|
||||
testConfigs.controlPluginCssWithOverrides.schema.properties
|
||||
.cssWithOverrides.formControlId,
|
||||
control: (config: ControlConfig): React.ReactNode => {
|
||||
return (
|
||||
<CSSControl
|
||||
css={(properties as unknown) as CSSPropertiesDictionary}
|
||||
key={`${config.dictionaryId}::${config.dataLocation}`}
|
||||
cssControls={[
|
||||
new CSSStandardControlPlugin({
|
||||
id: "foo",
|
||||
propertyNames: ["align-content", "align-items"],
|
||||
control: (config: CSSControlConfig) => {
|
||||
const handleChange = (
|
||||
propertyName: string
|
||||
): ((
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => void) => {
|
||||
return (
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
config.onChange({
|
||||
[propertyName]: e.target.value,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor={"align-content"}>
|
||||
align-content
|
||||
</label>
|
||||
<br />
|
||||
<input
|
||||
id={"align-content"}
|
||||
onChange={handleChange(
|
||||
"align-content"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={"align-items"}>
|
||||
align-items
|
||||
</label>
|
||||
<br />
|
||||
<input
|
||||
id={"align-items"}
|
||||
onChange={handleChange(
|
||||
"align-items"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
{...config}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const exampleData: any = getDataFromSchema(testConfigs.controlPluginCss.schema);
|
||||
|
||||
if ((window as any).Worker) {
|
||||
fastMessageSystem = new MessageSystem({
|
||||
webWorker: "message-system.js",
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: testConfigs.controlPluginCss.schema.id,
|
||||
data: exampleData,
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
ajvMapper = new AjvMapper({
|
||||
messageSystem: fastMessageSystem,
|
||||
});
|
||||
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
|
||||
}
|
||||
|
||||
this.state = {
|
||||
schema: testConfigs.controlPluginCss.schema,
|
||||
data: exampleData,
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: testConfigs.controlPluginCss.schema.id,
|
||||
data: exampleData,
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
navigation: void 0,
|
||||
showExtendedControls: false,
|
||||
inlineErrors: void 0,
|
||||
defaultBrowserErrors: void 0,
|
||||
dataSet: dataSets[0].data,
|
||||
cssPropertyOverrides: false,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
|
||||
<div
|
||||
style={{
|
||||
width: "300px",
|
||||
height: "100vh",
|
||||
float: "left",
|
||||
fontFamily:
|
||||
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
<Form {...this.coerceFormProps()} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
float: "left",
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<select
|
||||
onChange={this.handleComponentUpdate}
|
||||
defaultValue={testConfigs.controlPluginCss.schema.id}
|
||||
>
|
||||
{this.getComponentOptions()}
|
||||
</select>
|
||||
{this.renderDataSetComponentOptions()}
|
||||
<br />
|
||||
<br />
|
||||
<input
|
||||
id={"useCSSOverrides"}
|
||||
type={"checkbox"}
|
||||
value={this.state.cssPropertyOverrides.toString()}
|
||||
onChange={this.handleCSSOverrideUpdate}
|
||||
/>
|
||||
<label htmlFor={"useCSSOverrides"}>
|
||||
Show CSS property overrides
|
||||
</label>
|
||||
<br />
|
||||
<input
|
||||
id={"showInlineErrors"}
|
||||
type="checkbox"
|
||||
value={(!!this.state.inlineErrors).toString()}
|
||||
onChange={this.handleShowInlineErrors}
|
||||
/>
|
||||
<label htmlFor={"showInlineErrors"}>Show inline errors</label>
|
||||
<br />
|
||||
<input
|
||||
id={"showBrowserErrors"}
|
||||
type="checkbox"
|
||||
value={(!!this.state.defaultBrowserErrors).toString()}
|
||||
onChange={this.handleShowBrowserErrors}
|
||||
/>
|
||||
<label htmlFor={"showBrowserErrors"}>
|
||||
Show default browser errors
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
<h2>Data Dictionary</h2>
|
||||
<pre
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgb(244, 245, 246)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(this.state.dataDictionary, null, 2)}
|
||||
</pre>
|
||||
<h2>Navigation</h2>
|
||||
<pre
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgb(244, 245, 246)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(this.state.navigation, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDataSetComponentOptions(): React.ReactNode {
|
||||
if (this.state.schema.id === testConfigs.allControlTypes.schema.id) {
|
||||
return (
|
||||
<select onChange={this.handleDataSetUpdate}>
|
||||
{this.getComponentDataSets()}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private generateSchemaDictionary(): SchemaDictionary {
|
||||
const schemaDictionary: SchemaDictionary = {};
|
||||
|
||||
Object.keys(testConfigs).forEach((testConfigKey: string) => {
|
||||
schemaDictionary[testConfigs[testConfigKey].schema.id] =
|
||||
testConfigs[testConfigKey].schema;
|
||||
});
|
||||
|
||||
return schemaDictionary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child options for the schema form
|
||||
*/
|
||||
private getChildOptions(): FormChildOptionItem[] {
|
||||
const childOptions: FormChildOptionItem[] = [];
|
||||
const groups: GroupItem[] = [
|
||||
{
|
||||
items: testConfigs,
|
||||
type: "components",
|
||||
},
|
||||
];
|
||||
|
||||
for (const group of groups) {
|
||||
Object.keys(group.items).map((itemName: any, key: number): void => {
|
||||
if (typeof testConfigs[itemName].schema !== "undefined") {
|
||||
const childObj: FormChildOptionItem = {
|
||||
name: testConfigs[itemName].schema.title || "Untitled",
|
||||
component: testConfigs[itemName].component,
|
||||
schema: testConfigs[itemName].schema,
|
||||
};
|
||||
|
||||
childOptions.push(childObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return childOptions;
|
||||
}
|
||||
|
||||
private getComponentDataSets(): React.ReactNode {
|
||||
return dataSets.map((dataSet: DataSet, index: number) => {
|
||||
return (
|
||||
<option key={index} value={index}>
|
||||
{dataSet.displayName}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private coerceFormProps(): FormProps {
|
||||
const formProps: FormProps = {
|
||||
messageSystem: fastMessageSystem,
|
||||
controls: this.controlPlugins,
|
||||
categories: {
|
||||
category: {
|
||||
"": [
|
||||
{
|
||||
title: "String & Boolean",
|
||||
dataLocations: ["string", "boolean"],
|
||||
},
|
||||
{
|
||||
title: "Empty",
|
||||
dataLocations: [],
|
||||
},
|
||||
{
|
||||
title: "No match",
|
||||
dataLocations: ["foo", "bar"],
|
||||
},
|
||||
{
|
||||
title: "Advanced",
|
||||
dataLocations: ["array", "object"],
|
||||
expandByDefault: false,
|
||||
},
|
||||
],
|
||||
object: [
|
||||
{
|
||||
title: "Test",
|
||||
dataLocations: ["object.string"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof this.state.defaultBrowserErrors === "boolean") {
|
||||
formProps.displayValidationBrowserDefault = this.state.defaultBrowserErrors;
|
||||
}
|
||||
|
||||
if (typeof this.state.inlineErrors === "boolean") {
|
||||
formProps.displayValidationInline = this.state.inlineErrors;
|
||||
}
|
||||
|
||||
return formProps;
|
||||
}
|
||||
|
||||
private handleMessageSystem = (e: MessageEvent): void => {
|
||||
switch (e.data.type) {
|
||||
case MessageSystemType.initialize:
|
||||
if (e.data.data && e.data.navigation) {
|
||||
this.setState({
|
||||
data: e.data.data,
|
||||
navigation: e.data.navigation,
|
||||
});
|
||||
}
|
||||
case MessageSystemType.data:
|
||||
if (e.data.data) {
|
||||
this.setState({
|
||||
data: e.data.data,
|
||||
dataDictionary: e.data.dataDictionary,
|
||||
});
|
||||
}
|
||||
case MessageSystemType.navigation:
|
||||
if (e.data.navigation) {
|
||||
this.setState({
|
||||
navigation: e.data.navigation,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleCSSOverrideUpdate = (): void => {
|
||||
this.setState({
|
||||
cssPropertyOverrides: !this.state.cssPropertyOverrides,
|
||||
});
|
||||
};
|
||||
|
||||
private handleDataSetUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
this.setState({
|
||||
data: dataSets[parseInt(e.target.value, 10)].data,
|
||||
});
|
||||
};
|
||||
|
||||
private handleShowInlineErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (e.target.value === "true") {
|
||||
this.setState({
|
||||
inlineErrors: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
inlineErrors: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleShowBrowserErrors = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (e.target.value === "true") {
|
||||
this.setState({
|
||||
defaultBrowserErrors: false,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
defaultBrowserErrors: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
const data: any = !!testConfigs[e.target.value].data
|
||||
? testConfigs[e.target.value].data
|
||||
: testConfigs[e.target.value].schema.id ===
|
||||
testConfigs.allControlTypes.schema.id
|
||||
? this.state.dataSet
|
||||
: getDataFromSchema(testConfigs[e.target.value].schema);
|
||||
|
||||
if ((window as any).Worker && fastMessageSystem) {
|
||||
fastMessageSystem.postMessage({
|
||||
type: MessageSystemType.initialize,
|
||||
data: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: testConfigs[e.target.value].schema.id,
|
||||
data,
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private getComponentOptions(): JSX.Element[] {
|
||||
return Object.keys(testConfigs).map((testComponentKey: any, index: number) => {
|
||||
return <option key={index}>{testConfigs[testComponentKey].schema.id}</option>;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { FormTestPage };
|
|
@ -0,0 +1,7 @@
|
|||
# App configs
|
||||
The files in this folder are for testing the form.
|
||||
|
||||
There should be:
|
||||
- A schema conforming to a sample set of data
|
||||
- An optional React component as an `index.tsx` file
|
||||
- An optional configuration file for extra options specified in the root README.mds
|
|
@ -0,0 +1,143 @@
|
|||
export interface ExampleComponent {
|
||||
schema: any;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
import {
|
||||
allControlTypesSchema,
|
||||
anyOfSchema,
|
||||
arraysSchema,
|
||||
badgeSchema,
|
||||
categorySchema,
|
||||
checkboxSchema,
|
||||
childrenSchema,
|
||||
constSchema as constKeywordSchema,
|
||||
controlPluginCssSchema,
|
||||
controlPluginCssWithOverridesSchema,
|
||||
controlPluginSchema as customControlSchema,
|
||||
defaultsSchema,
|
||||
dictionarySchema,
|
||||
disabledSchema,
|
||||
generalSchema,
|
||||
invalidDataSchema,
|
||||
mergedOneOfSchema,
|
||||
nestedOneOfSchema,
|
||||
nullSchema as nullKeywordSchema,
|
||||
numberFieldSchema,
|
||||
objectsSchema,
|
||||
oneOfDeeplyNestedSchema as oneOfArraysSchema,
|
||||
oneOfSchema,
|
||||
textareaSchema,
|
||||
textSchema,
|
||||
tooltipSchema,
|
||||
} from "../../../src/__tests__/schemas";
|
||||
|
||||
export const category: ExampleComponent = {
|
||||
schema: categorySchema,
|
||||
};
|
||||
|
||||
export const customControl: ExampleComponent = {
|
||||
schema: customControlSchema,
|
||||
};
|
||||
|
||||
export const controlPluginCssWithOverrides: ExampleComponent = {
|
||||
schema: controlPluginCssWithOverridesSchema,
|
||||
};
|
||||
|
||||
export const controlPluginCss: ExampleComponent = {
|
||||
schema: controlPluginCssSchema,
|
||||
};
|
||||
|
||||
export const textField: ExampleComponent = {
|
||||
schema: textareaSchema,
|
||||
};
|
||||
|
||||
export const text: ExampleComponent = {
|
||||
schema: textSchema,
|
||||
};
|
||||
|
||||
export const numberField: ExampleComponent = {
|
||||
schema: numberFieldSchema,
|
||||
};
|
||||
|
||||
export const checkbox: ExampleComponent = {
|
||||
schema: checkboxSchema,
|
||||
};
|
||||
|
||||
export const anyOf: ExampleComponent = {
|
||||
schema: anyOfSchema,
|
||||
};
|
||||
|
||||
export const oneOf: ExampleComponent = {
|
||||
schema: oneOfSchema,
|
||||
};
|
||||
|
||||
export const nestedOneOf: ExampleComponent = {
|
||||
schema: nestedOneOfSchema,
|
||||
};
|
||||
|
||||
export const mergedOneOf: ExampleComponent = {
|
||||
schema: mergedOneOfSchema,
|
||||
};
|
||||
|
||||
export const objects: ExampleComponent = {
|
||||
schema: objectsSchema,
|
||||
};
|
||||
|
||||
export const arrays: ExampleComponent = {
|
||||
schema: arraysSchema,
|
||||
};
|
||||
|
||||
export const oneOfArrays: ExampleComponent = {
|
||||
schema: oneOfArraysSchema,
|
||||
};
|
||||
|
||||
export const children: ExampleComponent = {
|
||||
schema: childrenSchema,
|
||||
};
|
||||
|
||||
export const generalExample: ExampleComponent = {
|
||||
schema: generalSchema,
|
||||
};
|
||||
|
||||
export const badge: ExampleComponent = {
|
||||
schema: badgeSchema,
|
||||
};
|
||||
|
||||
export const constKeyword: ExampleComponent = {
|
||||
schema: constKeywordSchema,
|
||||
};
|
||||
|
||||
import InvalidDataDataSet from "../../../src/__tests__/datasets/invalid-data";
|
||||
|
||||
export const invalidData: ExampleComponent = {
|
||||
schema: invalidDataSchema,
|
||||
data: InvalidDataDataSet,
|
||||
};
|
||||
|
||||
export const defaults: ExampleComponent = {
|
||||
schema: defaultsSchema,
|
||||
};
|
||||
|
||||
export const nullKeyword: ExampleComponent = {
|
||||
schema: nullKeywordSchema,
|
||||
};
|
||||
|
||||
export const allControlTypes: ExampleComponent = {
|
||||
schema: allControlTypesSchema,
|
||||
};
|
||||
|
||||
import DictionaryDataSet from "../../../src/__tests__/datasets/dictionary";
|
||||
|
||||
export const dictionary: ExampleComponent = {
|
||||
schema: dictionarySchema,
|
||||
data: DictionaryDataSet,
|
||||
};
|
||||
|
||||
export const tooltip: ExampleComponent = {
|
||||
schema: tooltipSchema,
|
||||
};
|
||||
|
||||
export const disabled: ExampleComponent = {
|
||||
schema: disabledSchema,
|
||||
};
|
|
@ -0,0 +1,165 @@
|
|||
import React from "react";
|
||||
import { MenuItem, NavigationMenu } from "../../src";
|
||||
import {
|
||||
accentColorName,
|
||||
L1ColorName,
|
||||
L4ColorName,
|
||||
textColorName,
|
||||
L3FillColorName,
|
||||
inactiveTextColorName,
|
||||
L3OutlineColorName,
|
||||
} from "../../src/style";
|
||||
|
||||
const menu: MenuItem[] = [
|
||||
{
|
||||
displayName: "foo",
|
||||
location: "foo",
|
||||
},
|
||||
{
|
||||
displayName: "bar",
|
||||
location: "bar",
|
||||
items: [
|
||||
{
|
||||
displayName: "bar1",
|
||||
location: "bar1",
|
||||
},
|
||||
{
|
||||
displayName: "bar2",
|
||||
items: [
|
||||
{
|
||||
displayName: "bar2_1",
|
||||
location: "bar2_1",
|
||||
items: [
|
||||
{
|
||||
displayName: "bar2_1_1",
|
||||
location: "bar2_1_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: "bat",
|
||||
location: "bat",
|
||||
},
|
||||
];
|
||||
|
||||
const CSSpropertyOverrides = {
|
||||
[accentColorName]: "blue",
|
||||
[L1ColorName]: "white",
|
||||
[L4ColorName]: "lightslategray",
|
||||
[textColorName]: "darkred",
|
||||
[L3FillColorName]: "white",
|
||||
[inactiveTextColorName]: "orange",
|
||||
[L3OutlineColorName]: "orange",
|
||||
};
|
||||
|
||||
export interface NavigationMenuTestPageState {
|
||||
expanded: boolean;
|
||||
location?: string;
|
||||
triggerLocationUpdate?: boolean;
|
||||
cssPropertyOverrides: boolean;
|
||||
}
|
||||
|
||||
class NavigationMenuTestPage extends React.Component<{}, NavigationMenuTestPageState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expanded: void 0,
|
||||
triggerLocationUpdate: false,
|
||||
cssPropertyOverrides: false,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
|
||||
<button onClick={this.handleExpandClick}>expand</button>
|
||||
<button onClick={this.handleCollapseClick}>collapse</button>
|
||||
<button
|
||||
onClick={this.handleTriggerLocationUpdate}
|
||||
style={this.getStyle("triggerLocationUpdate")}
|
||||
>
|
||||
manually trigger location update
|
||||
</button>
|
||||
<input
|
||||
id={"useCSSOverrides"}
|
||||
type={"checkbox"}
|
||||
value={this.state.cssPropertyOverrides.toString()}
|
||||
onChange={this.handleCSSOverrideUpdate}
|
||||
/>
|
||||
<label htmlFor={"useCSSOverrides"}>Show CSS property overrides</label>
|
||||
<NavigationMenu
|
||||
menu={menu}
|
||||
expanded={this.state.expanded}
|
||||
activeLocation={this.state.location}
|
||||
onLocationUpdate={
|
||||
this.state.triggerLocationUpdate
|
||||
? this.handleLocationUpdate
|
||||
: void 0
|
||||
}
|
||||
/>
|
||||
<pre>location - {this.state.location}</pre>
|
||||
<pre>{JSON.stringify(menu, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleCSSOverrideUpdate = (): void => {
|
||||
this.setState({
|
||||
cssPropertyOverrides: !this.state.cssPropertyOverrides,
|
||||
});
|
||||
};
|
||||
|
||||
private handleExpandClick = (): void => {
|
||||
this.setState(
|
||||
{
|
||||
expanded: true,
|
||||
},
|
||||
() => {
|
||||
this.setState({
|
||||
expanded: void 0,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private handleCollapseClick = (): void => {
|
||||
this.setState(
|
||||
{
|
||||
expanded: false,
|
||||
},
|
||||
() => {
|
||||
this.setState({
|
||||
expanded: void 0,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
private handleLocationUpdate = (location: string): void => {
|
||||
this.setState({
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
private handleTriggerLocationUpdate = (): void => {
|
||||
this.setState({
|
||||
triggerLocationUpdate: !this.state.triggerLocationUpdate,
|
||||
});
|
||||
};
|
||||
|
||||
private getStyle(stateKey: string): any {
|
||||
if (this.state[stateKey]) {
|
||||
return {
|
||||
background: "#414141",
|
||||
color: "white",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { NavigationMenuTestPage };
|
|
@ -0,0 +1,291 @@
|
|||
import React from "react";
|
||||
import { ModularNavigation } from "../../src";
|
||||
import childrenSchema from "./navigation/children.schema";
|
||||
import { children } from "./navigation/example.data";
|
||||
import { DataType, MessageSystem, MessageSystemType } from "@microsoft/fast-tooling";
|
||||
import noChildrenSchema from "./navigation/no-children.schema";
|
||||
import {
|
||||
accentColorName,
|
||||
L1ColorName,
|
||||
L3ColorName,
|
||||
textColorName,
|
||||
L3FillColorName,
|
||||
inactiveTextColorName,
|
||||
L3OutlineColorName,
|
||||
} from "../../src/style";
|
||||
import { Data } from "@microsoft/fast-tooling";
|
||||
import { MessageSystemNavigationTypeAction } from "@microsoft/fast-tooling";
|
||||
|
||||
export interface NavigationTestPageState {
|
||||
navigation: any;
|
||||
cssPropertyOverrides: boolean;
|
||||
types?: DataType[];
|
||||
activeDictionaryId: string;
|
||||
defaultLinkedDataDroppableDataLocation: boolean;
|
||||
droppableBlocklist: string[];
|
||||
}
|
||||
|
||||
const CSSpropertyOverrides = {
|
||||
[accentColorName]: "blue",
|
||||
[L1ColorName]: "white",
|
||||
[L3ColorName]: "lightslategray",
|
||||
[textColorName]: "darkred",
|
||||
[L3FillColorName]: "white",
|
||||
[inactiveTextColorName]: "orange",
|
||||
[L3OutlineColorName]: "orange",
|
||||
};
|
||||
|
||||
let fastMessageSystem: MessageSystem;
|
||||
|
||||
class NavigationTestPage extends React.Component<{}, NavigationTestPageState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
if ((window as any).Worker) {
|
||||
fastMessageSystem = new MessageSystem({
|
||||
webWorker: "message-system.js",
|
||||
dataDictionary: children,
|
||||
schemaDictionary: {
|
||||
[childrenSchema.id]: childrenSchema,
|
||||
[noChildrenSchema.id]: noChildrenSchema,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fastMessageSystem.add({
|
||||
onMessage: this.handleMessageSystem,
|
||||
});
|
||||
|
||||
this.state = {
|
||||
navigation: null,
|
||||
cssPropertyOverrides: false,
|
||||
types: undefined,
|
||||
activeDictionaryId: children[1],
|
||||
defaultLinkedDataDroppableDataLocation: false,
|
||||
droppableBlocklist: [],
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div style={this.state.cssPropertyOverrides ? CSSpropertyOverrides : {}}>
|
||||
<input
|
||||
id={"useCSSOverrides"}
|
||||
type={"checkbox"}
|
||||
value={this.state.cssPropertyOverrides.toString()}
|
||||
onChange={this.handleCSSOverrideUpdate}
|
||||
/>
|
||||
<label htmlFor={"useCSSOverrides"}>Show CSS property overrides</label>
|
||||
<br />
|
||||
<fieldset>
|
||||
<legend>Allow types</legend>
|
||||
<input
|
||||
id={"allowArrays"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.array)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.array)}
|
||||
/>
|
||||
<label htmlFor={"allowArrays"}>Allow arrays</label>
|
||||
<br />
|
||||
<input
|
||||
id={"allowObjects"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.object)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.object)}
|
||||
/>
|
||||
<label htmlFor={"allowArrays"}>Allow objects</label>
|
||||
<br />
|
||||
<input
|
||||
id={"allowBooleans"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.boolean)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.boolean)}
|
||||
/>
|
||||
<label htmlFor={"allowBooleans"}>Allow booleans</label>
|
||||
<br />
|
||||
<input
|
||||
id={"allowStrings"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.string)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.string)}
|
||||
/>
|
||||
<label htmlFor={"allowStrings"}>Allow strings</label>
|
||||
<br />
|
||||
<input
|
||||
id={"allowNumbers"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.number)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.number)}
|
||||
/>
|
||||
<label htmlFor={"allowNumbers"}>Allow numbers</label>
|
||||
<br />
|
||||
<input
|
||||
id={"allowNulls"}
|
||||
type={"checkbox"}
|
||||
value={`${
|
||||
Array.isArray(this.state.types) &&
|
||||
!!this.state.types.find(type => type === DataType.null)
|
||||
}`}
|
||||
onChange={this.handleIncludeType(DataType.null)}
|
||||
/>
|
||||
<label htmlFor={"allowNulls"}>Allow null</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Options</legend>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
id={"assignDefaultLinkedDataDatalocation"}
|
||||
onChange={this.handleSetDefaultLinkedDataDatalocation}
|
||||
value={this.state.defaultLinkedDataDroppableDataLocation.toString()}
|
||||
/>
|
||||
<label htmlFor={"assignDefaultLinkedDataDatalocation"}>
|
||||
Assign "children" as default data location
|
||||
</label>
|
||||
<br />
|
||||
<input
|
||||
type={"checkbox"}
|
||||
id={"assignDroppableBlocklist"}
|
||||
onChange={this.handleSetDroppableBlocklist}
|
||||
value={this.state.droppableBlocklist}
|
||||
/>
|
||||
<label htmlFor={"assignDroppableBlocklist"}>
|
||||
Assign linked data without a default slot to the blocklist
|
||||
</label>
|
||||
</fieldset>
|
||||
{this.renderAllLinkedData()}
|
||||
<ModularNavigation
|
||||
messageSystem={fastMessageSystem}
|
||||
types={this.state.types}
|
||||
defaultLinkedDataDroppableDataLocation={
|
||||
this.state.defaultLinkedDataDroppableDataLocation
|
||||
? "children"
|
||||
: void 0
|
||||
}
|
||||
droppableBlocklist={this.state.droppableBlocklist}
|
||||
/>
|
||||
|
||||
<pre>{JSON.stringify(this.state.types, null, 2)}</pre>
|
||||
<pre>{JSON.stringify(this.state.navigation, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAllLinkedData(): React.ReactNode {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>Linked data IDs</legend>
|
||||
{this.renderLinkedDataItems()}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLinkedDataItems(): React.ReactNode {
|
||||
return Object.entries(children[0]).map(
|
||||
([key, child]: [string, Data<unknown>], index: number) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<label htmlFor={key}>{key}</label>
|
||||
<input
|
||||
id={key}
|
||||
name={"linked-data"}
|
||||
type={"radio"}
|
||||
onChange={this.handleLinkedDataNavigationOnChange(key)}
|
||||
value={key}
|
||||
checked={this.state.activeDictionaryId === key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleSetDefaultLinkedDataDatalocation = (e: React.ChangeEvent): void => {
|
||||
this.setState({
|
||||
defaultLinkedDataDroppableDataLocation: !this.state
|
||||
.defaultLinkedDataDroppableDataLocation,
|
||||
});
|
||||
};
|
||||
|
||||
private handleSetDroppableBlocklist = (e: React.ChangeEvent): void => {
|
||||
this.setState({
|
||||
droppableBlocklist:
|
||||
this.state.droppableBlocklist.length > 0 ? [] : [noChildrenSchema.$id],
|
||||
});
|
||||
};
|
||||
|
||||
private handleLinkedDataNavigationOnChange = (linkedDataId: string): (() => void) => {
|
||||
return () => {
|
||||
fastMessageSystem.postMessage({
|
||||
type: MessageSystemType.navigation,
|
||||
action: MessageSystemNavigationTypeAction.update,
|
||||
activeDictionaryId: linkedDataId,
|
||||
activeNavigationConfigId: "",
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
private handleIncludeType = (
|
||||
type: DataType
|
||||
): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
let currentTypes: DataType[] = [].concat(this.state.types || []);
|
||||
|
||||
if (e.target.checked) {
|
||||
currentTypes.push(type);
|
||||
} else {
|
||||
const indexOfType = currentTypes.findIndex(
|
||||
currentType => currentType === type
|
||||
);
|
||||
|
||||
currentTypes.splice(indexOfType, 1);
|
||||
}
|
||||
|
||||
if (currentTypes.length === 0) {
|
||||
currentTypes = undefined;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
types: currentTypes,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
private handleMessageSystem = (e: MessageEvent): void => {
|
||||
if (
|
||||
e.data &&
|
||||
(e.data.type === MessageSystemType.initialize ||
|
||||
e.data.type === MessageSystemType.data)
|
||||
) {
|
||||
this.setState({
|
||||
navigation: e.data.navigationDictionary,
|
||||
});
|
||||
} else if (e.data && e.data.type === MessageSystemType.navigation) {
|
||||
this.setState({
|
||||
activeDictionaryId: e.data.activeDictionaryId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleCSSOverrideUpdate = (): void => {
|
||||
this.setState({
|
||||
cssPropertyOverrides: !this.state.cssPropertyOverrides,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { NavigationTestPage };
|
|
@ -0,0 +1,40 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with children",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "children",
|
||||
$id: "children",
|
||||
properties: {
|
||||
object: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
title: "Object Children",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
array: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
title: "Array Children",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
children: {
|
||||
title: "Children",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
import { DataDictionary } from "@microsoft/fast-tooling";
|
||||
import noChildrenSchema from "./no-children.schema";
|
||||
import childrenSchema from "./children.schema";
|
||||
|
||||
const noChildren: any = {
|
||||
text: "Hello world",
|
||||
};
|
||||
const children: DataDictionary<any> = [
|
||||
{
|
||||
foo: {
|
||||
schemaId: childrenSchema.id,
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bar",
|
||||
},
|
||||
{
|
||||
id: "bat",
|
||||
},
|
||||
{
|
||||
id: "baz",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
schemaId: noChildrenSchema.id,
|
||||
data: {
|
||||
text: "bar",
|
||||
},
|
||||
},
|
||||
bat: {
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
schemaId: noChildrenSchema.id,
|
||||
data: {
|
||||
text: "bat",
|
||||
},
|
||||
},
|
||||
baz: {
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
schemaId: childrenSchema.id,
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bax",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bax: {
|
||||
parent: {
|
||||
id: "baz",
|
||||
dataLocation: "children",
|
||||
},
|
||||
schemaId: noChildrenSchema.id,
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
];
|
||||
|
||||
export { children, noChildren };
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component without children",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "no-children",
|
||||
$id: "no-children",
|
||||
properties: {
|
||||
text: {
|
||||
title: "Text",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import React from "react";
|
||||
import { MessageSystemType } from "@microsoft/fast-tooling";
|
||||
import { ViewerCustomAction } from "../../../src";
|
||||
|
||||
interface ViewerContentState {
|
||||
message: string;
|
||||
}
|
||||
|
||||
class ViewerContent extends React.Component<{}, ViewerContentState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: "Hello world",
|
||||
};
|
||||
|
||||
window.addEventListener("message", this.handlePostMessage);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<pre>{this.state.message}</pre>
|
||||
<button onClick={this.handleReset}>reset</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handleReset = (): void => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: MessageSystemType.custom,
|
||||
action: ViewerCustomAction.call,
|
||||
value: "reset",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
private handlePostMessage = (e: MessageEvent): void => {
|
||||
if (e.origin === location.origin && typeof e.data === "string") {
|
||||
try {
|
||||
this.setState({
|
||||
message: JSON.stringify(JSON.parse(e.data), null, 2),
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default ViewerContent;
|
|
@ -0,0 +1,160 @@
|
|||
import React from "react";
|
||||
import { MessageSystem, MessageSystemType } from "@microsoft/fast-tooling";
|
||||
import {
|
||||
defaultDevices,
|
||||
Device,
|
||||
Display,
|
||||
Orientation,
|
||||
Rotate,
|
||||
SelectDevice,
|
||||
Viewer,
|
||||
} from "../../../src";
|
||||
|
||||
export interface PageState {
|
||||
height: number;
|
||||
width: number;
|
||||
activeDevice: Device;
|
||||
orientation: Orientation;
|
||||
inputValue: string;
|
||||
}
|
||||
|
||||
let fastMessageSystem: MessageSystem;
|
||||
|
||||
class ViewerPage extends React.Component<{}, PageState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
if ((window as any).Worker) {
|
||||
fastMessageSystem = new MessageSystem({
|
||||
webWorker: "message-system.js",
|
||||
dataDictionary: [
|
||||
{
|
||||
"": {
|
||||
schemaId: "foo",
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
"",
|
||||
],
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
|
||||
}
|
||||
|
||||
this.state = {
|
||||
height: 800,
|
||||
width: 800,
|
||||
activeDevice: defaultDevices[0],
|
||||
orientation: Orientation.portrait,
|
||||
inputValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div style={{ width: "100%", height: "calc(100vh - 200px)" }}>
|
||||
<div style={{ margin: "10px 0" }}>
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.handleInputUpdate}
|
||||
value={this.state.inputValue}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<SelectDevice
|
||||
devices={defaultDevices}
|
||||
onUpdateDevice={this.handleDeviceUpdate}
|
||||
activeDeviceId={this.state.activeDevice.id}
|
||||
jssStyleSheet={{
|
||||
selectDevice: {
|
||||
paddingRight: "10px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Rotate
|
||||
orientation={this.state.orientation}
|
||||
onUpdateOrientation={this.handleOrientationUpdate}
|
||||
landscapeDisabled={this.isRotateDisabled()}
|
||||
portraitDisabled={this.isRotateDisabled()}
|
||||
/>
|
||||
</div>
|
||||
<Viewer
|
||||
height={this.state.height}
|
||||
width={this.state.width}
|
||||
iframeSrc={"/viewer/content"}
|
||||
responsive={this.state.activeDevice.display === Display.responsive}
|
||||
onUpdateHeight={this.handleUpdatedHeight}
|
||||
onUpdateWidth={this.handleUpdatedWidth}
|
||||
messageSystem={fastMessageSystem}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private isRotateDisabled(): boolean {
|
||||
return !!!this.state.activeDevice.width && !!!this.state.activeDevice.height;
|
||||
}
|
||||
|
||||
private handleMessageSystem = (e: MessageEvent): void => {
|
||||
if (e.data.type === MessageSystemType.custom) {
|
||||
this.setState({
|
||||
inputValue: e.data.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleInputUpdate = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
fastMessageSystem.postMessage({
|
||||
type: MessageSystemType.custom,
|
||||
value: e.target.value,
|
||||
} as any);
|
||||
};
|
||||
|
||||
private handleOrientationUpdate = (orientation: Orientation): void => {
|
||||
if (!this.isRotateDisabled()) {
|
||||
this.setState({
|
||||
orientation,
|
||||
width:
|
||||
orientation === Orientation.portrait
|
||||
? this.state.activeDevice.width
|
||||
: this.state.activeDevice.height,
|
||||
height:
|
||||
orientation === Orientation.portrait
|
||||
? this.state.activeDevice.height
|
||||
: this.state.activeDevice.width,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleDeviceUpdate = (deviceId: string): void => {
|
||||
const activeDevice: Device = defaultDevices.find((device: Device) => {
|
||||
return deviceId === device.id;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
activeDevice,
|
||||
orientation: Orientation.portrait,
|
||||
height: activeDevice.height ? activeDevice.height : this.state.height,
|
||||
width: activeDevice.width ? activeDevice.width : this.state.width,
|
||||
});
|
||||
};
|
||||
|
||||
private handleUpdatedHeight = (height: number): void => {
|
||||
this.setState({
|
||||
height,
|
||||
});
|
||||
};
|
||||
|
||||
private handleUpdatedWidth = (width: number): void => {
|
||||
this.setState({
|
||||
width,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default ViewerPage;
|
|
@ -0,0 +1,213 @@
|
|||
import { Form, Viewer } from "../../src";
|
||||
import { FormProps } from "../../src/form/form.props";
|
||||
import { DesignSystemProvider } from "@microsoft/fast-jss-manager-react";
|
||||
import React from "react";
|
||||
import {
|
||||
getDataFromSchema,
|
||||
MessageSystem,
|
||||
MessageSystemType,
|
||||
SchemaDictionary,
|
||||
} from "@microsoft/fast-tooling";
|
||||
import FancyButton from "./web-components/fancy-button";
|
||||
import { webComponentSchemas } from "./web-components/index";
|
||||
import fancyButtonSchema from "./web-components/fancy-button.schema";
|
||||
|
||||
export type componentDataOnChange = (e: React.ChangeEvent<HTMLFormElement>) => void;
|
||||
|
||||
export interface WebComponentTestPageState {
|
||||
data: any;
|
||||
navigation: any;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface GroupItem {
|
||||
items: any;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const designSystemDefaults: any = {
|
||||
foregroundColor: "#000",
|
||||
backgroundColor: "#FFF",
|
||||
brandColor: "#0078D4",
|
||||
};
|
||||
|
||||
let fastMessageSystem: MessageSystem;
|
||||
|
||||
class WebComponentTestPage extends React.Component<{}, WebComponentTestPageState> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
if ((window as any).Worker) {
|
||||
fastMessageSystem = new MessageSystem({
|
||||
webWorker: "message-system.js",
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: fancyButtonSchema.id,
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
fastMessageSystem.add({ onMessage: this.handleMessageSystem });
|
||||
}
|
||||
|
||||
this.state = {
|
||||
data: {},
|
||||
navigation: void 0,
|
||||
height: 500,
|
||||
width: 500,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<DesignSystemProvider designSystem={designSystemDefaults}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: "250px",
|
||||
height: "100vh",
|
||||
float: "left",
|
||||
fontFamily:
|
||||
"Segoe UI, SegoeUI, Helvetica Neue, Helvetica, Arial, sans-serif",
|
||||
}}
|
||||
>
|
||||
<Form {...this.coerceFormProps()} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
float: "left",
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<select onChange={this.handleComponentUpdate}>
|
||||
{this.getComponentOptions()}
|
||||
</select>
|
||||
</div>
|
||||
<Viewer
|
||||
messageSystem={fastMessageSystem}
|
||||
iframeSrc={"/web-components/content"}
|
||||
responsive={true}
|
||||
onUpdateHeight={this.handleViewerUpdateHeight}
|
||||
onUpdateWidth={this.handleViewerUpdateWidth}
|
||||
height={this.state.height}
|
||||
width={this.state.width}
|
||||
/>
|
||||
<h2>Data</h2>
|
||||
<pre
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgb(244, 245, 246)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(this.state.data, null, 2)}
|
||||
</pre>
|
||||
<h2>Navigation</h2>
|
||||
<pre
|
||||
style={{
|
||||
padding: "12px",
|
||||
background: "rgb(244, 245, 246)",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(this.state.navigation, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</DesignSystemProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private generateSchemaDictionary(): SchemaDictionary {
|
||||
const schemaDictionary: SchemaDictionary = {};
|
||||
|
||||
Object.keys(webComponentSchemas).forEach((webComponentSchemasKey: string) => {
|
||||
schemaDictionary[webComponentSchemas[webComponentSchemasKey].schema.id] =
|
||||
webComponentSchemas[webComponentSchemasKey].schema;
|
||||
});
|
||||
|
||||
return schemaDictionary;
|
||||
}
|
||||
|
||||
private coerceFormProps(): FormProps {
|
||||
const formProps: FormProps = {
|
||||
messageSystem: fastMessageSystem,
|
||||
};
|
||||
|
||||
return formProps;
|
||||
}
|
||||
|
||||
private handleViewerUpdateHeight = (height: number): void => {
|
||||
this.setState({ height });
|
||||
};
|
||||
|
||||
private handleViewerUpdateWidth = (width: number): void => {
|
||||
this.setState({ width });
|
||||
};
|
||||
|
||||
private handleMessageSystem = (e: MessageEvent): void => {
|
||||
switch (e.data.type) {
|
||||
case MessageSystemType.initialize:
|
||||
if (e.data.dataDictionary && e.data.navigationDictionary) {
|
||||
this.setState({
|
||||
data: e.data.dataDictionary,
|
||||
navigation: e.data.navigationDictionary,
|
||||
});
|
||||
}
|
||||
case MessageSystemType.data:
|
||||
if (e.data.dataDictionary) {
|
||||
this.setState({
|
||||
data: e.data.dataDictionary,
|
||||
});
|
||||
}
|
||||
case MessageSystemType.navigation:
|
||||
if (e.data.navigationDictionary) {
|
||||
this.setState({
|
||||
navigation: e.data.navigationDictionary,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleComponentUpdate = (e: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
const data: any = !!webComponentSchemas[e.target.value].data
|
||||
? webComponentSchemas[e.target.value].data
|
||||
: getDataFromSchema(webComponentSchemas[e.target.value].schema);
|
||||
|
||||
if ((window as any).Worker && fastMessageSystem) {
|
||||
fastMessageSystem.postMessage({
|
||||
type: MessageSystemType.initialize,
|
||||
data: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: webComponentSchemas[e.target.value].schema.id,
|
||||
data,
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
schemaDictionary: this.generateSchemaDictionary(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private getComponentOptions(): JSX.Element[] {
|
||||
return Object.keys(webComponentSchemas).map(
|
||||
(webComponentSchemasKey: any, index: number) => {
|
||||
return (
|
||||
<option key={index}>
|
||||
{webComponentSchemas[webComponentSchemasKey].schema.id}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { WebComponentTestPage };
|
|
@ -0,0 +1,17 @@
|
|||
import { linkedDataSchema, ReservedElementMappingKeyword } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Fancy button",
|
||||
[ReservedElementMappingKeyword.mapsToTagName]: "fancy-button",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "fancyButton",
|
||||
properties: {
|
||||
children: {
|
||||
title: "Default slot",
|
||||
[ReservedElementMappingKeyword.mapsToSlot]: "",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
export default class FancyButton extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
const wrapper = document.createElement("button");
|
||||
const defaultSlot = document.createElement("slot");
|
||||
wrapper.appendChild(defaultSlot);
|
||||
|
||||
shadow.appendChild(wrapper);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import fancyButtonSchema from "./fancy-button.schema";
|
||||
import textSchema from "./text.schema";
|
||||
|
||||
export enum TestDataType {
|
||||
element = "element",
|
||||
string = "string",
|
||||
}
|
||||
|
||||
const webComponentSchemas: any = {
|
||||
[fancyButtonSchema.id]: {
|
||||
data: {},
|
||||
type: TestDataType.element,
|
||||
schema: fancyButtonSchema,
|
||||
},
|
||||
[textSchema.id]: {
|
||||
data: "foo",
|
||||
type: TestDataType.string,
|
||||
schema: textSchema,
|
||||
},
|
||||
};
|
||||
|
||||
export { webComponentSchemas };
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Text",
|
||||
description: "A test component's schema definition.",
|
||||
type: "string",
|
||||
id: "text",
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
import React from "react";
|
||||
import {
|
||||
DataDictionary,
|
||||
htmlMapper,
|
||||
htmlResolver,
|
||||
mapDataDictionary,
|
||||
MessageSystemType,
|
||||
SchemaDictionary,
|
||||
} from "@microsoft/fast-tooling";
|
||||
import FancyButton from "./fancy-button";
|
||||
|
||||
interface WebComponentViewerContentState {
|
||||
message: string;
|
||||
dataDictionary: DataDictionary<unknown>;
|
||||
schemaDictionary: SchemaDictionary;
|
||||
activeDictionaryId: string;
|
||||
}
|
||||
|
||||
class WebComponentViewerContent extends React.Component<
|
||||
{},
|
||||
WebComponentViewerContentState
|
||||
> {
|
||||
private ref: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.ref = React.createRef();
|
||||
|
||||
this.state = {
|
||||
message: "Hello world",
|
||||
dataDictionary: null,
|
||||
schemaDictionary: {},
|
||||
activeDictionaryId: null,
|
||||
};
|
||||
|
||||
customElements.define("fancy-button", FancyButton);
|
||||
|
||||
window.addEventListener("message", this.handlePostMessage);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div ref={this.ref} />
|
||||
<pre>{this.state.message}</pre>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handlePostMessage = (e: MessageEvent): void => {
|
||||
if (e.origin === location.origin && typeof e.data === "string") {
|
||||
try {
|
||||
const parsedJSON = JSON.parse(e.data);
|
||||
|
||||
if (parsedJSON.type === MessageSystemType.initialize) {
|
||||
this.setState({
|
||||
message: JSON.stringify(parsedJSON, null, 2),
|
||||
dataDictionary: parsedJSON.dataDictionary,
|
||||
schemaDictionary: parsedJSON.schemaDictionary,
|
||||
activeDictionaryId: parsedJSON.activeDictionaryId,
|
||||
});
|
||||
} else if (parsedJSON.type === MessageSystemType.data) {
|
||||
const mappedData: HTMLElement = mapDataDictionary({
|
||||
dataDictionary: parsedJSON.dataDictionary as DataDictionary<any>,
|
||||
schemaDictionary: this.state.schemaDictionary,
|
||||
mapper: htmlMapper({
|
||||
version: 1,
|
||||
tags: [
|
||||
{
|
||||
name: "fancy-button",
|
||||
description: "A fancier button",
|
||||
attributes: [],
|
||||
slots: [
|
||||
{
|
||||
name: "",
|
||||
description: "The default slot",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
resolver: htmlResolver,
|
||||
});
|
||||
|
||||
this.ref.current.innerHTML = "";
|
||||
this.ref.current.append(mappedData);
|
||||
|
||||
this.setState({
|
||||
message: JSON.stringify(parsedJSON, null, 2),
|
||||
dataDictionary: parsedJSON.dataDictionary,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default WebComponentViewerContent;
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: {
|
||||
node: "current",
|
||||
},
|
||||
},
|
||||
],
|
||||
"@babel/react",
|
||||
],
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
{
|
||||
"name": "@microsoft/fast-tooling-react",
|
||||
"description": "A React-specific set of components and utilities to assist in creating web UI",
|
||||
"sideEffects": false,
|
||||
"version": "2.11.4",
|
||||
"author": {
|
||||
"name": "Microsoft",
|
||||
"url": "https://discord.gg/FcSNfg4"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Microsoft/fast.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Microsoft/fast/issues/new/choose"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p ./tsconfig.json",
|
||||
"build:app": "webpack --progress --mode=production",
|
||||
"clean:dist": "node ../../build/clean.js dist",
|
||||
"coverage": "jest --coverage",
|
||||
"prepublishOnly": "npm run clean:dist && npm run build",
|
||||
"postinstall": "npm run clean:dist && npm run build",
|
||||
"prettier": "prettier --config ../../.prettierrc --write \"**/*.{ts,tsx,html}\"",
|
||||
"prettier:diff": "prettier --config ../../.prettierrc \"**/*.{ts,tsx,html}\" --list-different",
|
||||
"start": "webpack-dev-server --history-api-fallback --progress --config webpack.config.cjs",
|
||||
"test": "npm run eslint && npm run unit-tests && npm run build",
|
||||
"eslint": "eslint . --ext .ts",
|
||||
"eslint:fix": "eslint . --ext .ts --fix",
|
||||
"unit-tests": "jest --runInBand",
|
||||
"watch": "npm run build -- -w --preserveWatchOutput"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"coverageReporters": [
|
||||
"json",
|
||||
"text",
|
||||
[
|
||||
"lcov",
|
||||
{
|
||||
"projectRoot": "../../"
|
||||
}
|
||||
]
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 80,
|
||||
"branches": 59,
|
||||
"functions": 74,
|
||||
"lines": 80
|
||||
}
|
||||
},
|
||||
"coveragePathIgnorePatterns": [
|
||||
"/(.tmp|__tests__)/*"
|
||||
],
|
||||
"testURL": "http://localhost",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"!<rootDir>/node_modules/lodash-es"
|
||||
],
|
||||
"testRegex": "(\\.|/)(test|spec)\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
],
|
||||
"testEnvironment": "jest-environment-jsdom-sixteen"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.13",
|
||||
"@babel/preset-env": "^7.12.13",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/node": "^9.6.7",
|
||||
"@types/react": "^16.8.0",
|
||||
"@types/react-router": "^4.0.0",
|
||||
"enzyme": "^3.7.0",
|
||||
"enzyme-adapter-react-16": "^1.7.0",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"focus-visible": "^4.1.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jest": "^25.4.0",
|
||||
"jest-environment-jsdom-sixteen": "^2.0.0",
|
||||
"lodash-es": "4.17.15",
|
||||
"prettier": "2.0.2",
|
||||
"react": "^16.8.0",
|
||||
"react-dnd-html5-backend": "^9.0.0",
|
||||
"react-dom": "^16.8.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-test-renderer": "^16.6.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^25.4.0",
|
||||
"ts-loader": "^4.0.1",
|
||||
"typescript": "^3.9.0",
|
||||
"webpack": "^4.44.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/fast-jss-manager-react": "^3.0.0 || ^4.0.0",
|
||||
"lodash-es": "^4.0.0",
|
||||
"react": "^16.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/fast-colors": "^5.1.3",
|
||||
"@microsoft/fast-components": "^2.9.2",
|
||||
"@microsoft/fast-components-class-name-contracts-base": "^4.8.0",
|
||||
"@microsoft/fast-components-foundation-react": "^3.2.0",
|
||||
"@microsoft/fast-element": "^1.5.1",
|
||||
"@microsoft/fast-foundation": "^2.13.1",
|
||||
"@microsoft/fast-jss-manager-react": "^3.0.0 || ^4.0.0",
|
||||
"@microsoft/fast-jss-utilities": "^4.8.0",
|
||||
"@microsoft/fast-tooling": "^0.29.0",
|
||||
"@microsoft/fast-web-utilities": "^4.8.1",
|
||||
"@skatejs/val": "^0.5.0",
|
||||
"exenv-es6": "^1.0.0",
|
||||
"raf-throttle": "^2.0.3",
|
||||
"react-dnd": "^9.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
additionalFalse: {
|
||||
foo: 1,
|
||||
bar: "bat",
|
||||
},
|
||||
ad: "foo",
|
||||
b: "bar",
|
||||
c: "bat",
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
validBooleanRequired: true,
|
||||
invalidBooleanWrongType: "foo",
|
||||
invalidNullWrongType: "bar",
|
||||
invalidStringWrongType: false,
|
||||
invalidNumberWrongType: "bar",
|
||||
invalidEnumWrongType: "hello",
|
||||
invalidObjectWrongType: true,
|
||||
invalidObjectMissingProperty: {
|
||||
foo: "bar",
|
||||
},
|
||||
invalidArrayWrongType: "world",
|
||||
objectExample: {
|
||||
invalidBooleanWrongType: "bat",
|
||||
},
|
||||
arrayExample: [true, "foo", false, "bar", 3],
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with horizontal alignment",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "alignHorizontal",
|
||||
properties: {
|
||||
alignHorizontal: {
|
||||
title: "Horizontal alignment",
|
||||
type: "string",
|
||||
enum: ["left", "center", "right"],
|
||||
},
|
||||
},
|
||||
required: ["alignHorizontal"],
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with all control types",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "allControlTypes",
|
||||
properties: {
|
||||
textarea: {
|
||||
title: "textarea",
|
||||
type: "string",
|
||||
},
|
||||
"section-link": {
|
||||
title: "section-link",
|
||||
type: "object",
|
||||
},
|
||||
display: {
|
||||
title: "display",
|
||||
const: "foobar",
|
||||
},
|
||||
checkbox: {
|
||||
title: "checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
button: {
|
||||
title: "button",
|
||||
type: "null",
|
||||
},
|
||||
array: {
|
||||
title: "array",
|
||||
items: {
|
||||
title: "array item",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
"number-field": {
|
||||
title: "number-field",
|
||||
type: "number",
|
||||
},
|
||||
select: {
|
||||
title: "select",
|
||||
type: "string",
|
||||
enum: ["foo", "bar", "bat"],
|
||||
},
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,170 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with anyOf",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "anyOf",
|
||||
anyOf: [
|
||||
{
|
||||
description: "String",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
string: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
number: {
|
||||
title: "Number",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Sub-object alpha",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
subObjectAlpha: {
|
||||
title: "Sub-object alpha",
|
||||
type: "object",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Sub-object alpha foo",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Sub-object beta",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
subObjectBeta: {
|
||||
title: "Sub-object beta",
|
||||
type: "object",
|
||||
properties: {
|
||||
bar: {
|
||||
title: "Sub-object alpha bar",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Nested anyOf",
|
||||
description: "Nested anyOf",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
nestedAnyOf: {
|
||||
title: "Nested anyOf",
|
||||
description: "Nested anyOf Configuration",
|
||||
anyOf: [
|
||||
{
|
||||
description: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
object: {
|
||||
title: "String",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
},
|
||||
required: ["object"],
|
||||
},
|
||||
{
|
||||
description: "String",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
{
|
||||
description: "Number",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
title: "Number",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["number"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
additionalProperties: false,
|
||||
description: "Number or String",
|
||||
type: "object",
|
||||
properties: {
|
||||
numberOrString: {
|
||||
anyOf: [
|
||||
{
|
||||
title: "Number",
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Array item",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Array with anyOf in items",
|
||||
type: "array",
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
additionalProperties: false,
|
||||
title: "Array item object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
title: "Array item object string",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Array item number",
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ["numberOrString"],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with array",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "arrays",
|
||||
properties: {
|
||||
strings: {
|
||||
title: "Array of strings",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
minItems: 2,
|
||||
maxItems: 5,
|
||||
},
|
||||
objects: {
|
||||
title: "Array of objects",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
},
|
||||
stringsWithDefault: {
|
||||
title: "Array of strings with default",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
default: ["foo", "bar"],
|
||||
},
|
||||
objectsWithDefault: {
|
||||
title: "Array of objects with default",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
default: [
|
||||
{
|
||||
string: "foo",
|
||||
},
|
||||
{
|
||||
string: "bar",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ["strings"],
|
||||
};
|
|
@ -0,0 +1,145 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Badge",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "badge",
|
||||
properties: {
|
||||
string: {
|
||||
title: "Textarea",
|
||||
badge: "info",
|
||||
badgeDescription: "More information is provided",
|
||||
type: "string",
|
||||
},
|
||||
boolean: {
|
||||
title: "Checkbox",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
type: "boolean",
|
||||
},
|
||||
enum: {
|
||||
title: "Select",
|
||||
badge: "locked",
|
||||
badgeDescription: "This field is locked",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
},
|
||||
number: {
|
||||
title: "Numberfield",
|
||||
badge: "info",
|
||||
badgeDescription: "More information is provided",
|
||||
type: "number",
|
||||
},
|
||||
object: {
|
||||
title: "Object (no required items)",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
array: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
items: {
|
||||
title: "String item",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
stringWithDefault: {
|
||||
title: "Textarea",
|
||||
badge: "info",
|
||||
badgeDescription: "More information is provided",
|
||||
type: "string",
|
||||
default: "Hello world",
|
||||
},
|
||||
booleanWithDefault: {
|
||||
title: "Checkbox",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
enumWithDefault: {
|
||||
title: "Select",
|
||||
badge: "locked",
|
||||
badgeDescription: "This field is locked",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
numberWithDefault: {
|
||||
title: "Numberfield",
|
||||
badge: "info",
|
||||
badgeDescription: "More information is provided",
|
||||
type: "number",
|
||||
default: 42,
|
||||
},
|
||||
objectWithDefault: {
|
||||
title: "Object (no required items)",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
number: 100,
|
||||
},
|
||||
},
|
||||
arrayWithDefault: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
items: {
|
||||
title: "String item",
|
||||
type: "string",
|
||||
},
|
||||
default: ["foo", "bar"],
|
||||
},
|
||||
constWithDefault: {
|
||||
title: "Display using const",
|
||||
badge: "locked",
|
||||
badgeDescription: "This field is locked",
|
||||
type: "string",
|
||||
const: "A",
|
||||
default: "B",
|
||||
},
|
||||
selectWithSingleItemWithDefault: {
|
||||
title: "Display using single enum",
|
||||
badge: "locked",
|
||||
badgeDescription: "This field is locked",
|
||||
type: "string",
|
||||
enum: ["A"],
|
||||
default: "B",
|
||||
},
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
},
|
||||
childrenWithDefault: {
|
||||
...linkedDataSchema,
|
||||
|
||||
badge: "warning",
|
||||
badgeDescription: "Warning message",
|
||||
default: {
|
||||
id: "textField",
|
||||
props: {
|
||||
text: "Hello world",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Category",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
$id: "category",
|
||||
id: "category",
|
||||
properties: {
|
||||
string: {
|
||||
title: "Textarea",
|
||||
type: "string",
|
||||
},
|
||||
boolean: {
|
||||
title: "Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
enum: {
|
||||
title: "Select",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
},
|
||||
number: {
|
||||
title: "Numberfield",
|
||||
type: "number",
|
||||
},
|
||||
object: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
string: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
array: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "String item",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
children: {
|
||||
title: "Linked data",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with checkbox",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "checkbox",
|
||||
properties: {
|
||||
toggle: {
|
||||
title: "Required Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
optionalToggle: {
|
||||
title: "Optional Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
defaultToggle: {
|
||||
title: "Default Checkbox",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
disabledToggle: {
|
||||
title: "Disabled Checkbox",
|
||||
type: "boolean",
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
required: ["toggle"],
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with custom properties ",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "children-with-react-props",
|
||||
properties: {
|
||||
boolean: {
|
||||
title: "Boolean",
|
||||
type: "boolean",
|
||||
pluginId: "boolean-plugin-resolver",
|
||||
},
|
||||
array: {
|
||||
title: "Array of strings",
|
||||
type: "array",
|
||||
pluginId: "array-plugin-resolver",
|
||||
items: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
arrayObject: {
|
||||
title: "Array of objects",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
content: {
|
||||
...linkedDataSchema,
|
||||
pluginId: "children-plugin-resolver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render: {
|
||||
...linkedDataSchema,
|
||||
|
||||
pluginId: "children-plugin-resolver",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with children",
|
||||
alias: "With Children",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "children",
|
||||
properties: {
|
||||
objectContainingNestedChildren: {
|
||||
title: "Object with nested children",
|
||||
type: "object",
|
||||
properties: {
|
||||
nestedObjectChildren: {
|
||||
title: "Children in object",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayContainingNestedChildren: {
|
||||
title: "Array with nested children",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Nested array item",
|
||||
type: "object",
|
||||
properties: {
|
||||
nestedArrayChildren: {
|
||||
title: "Children",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
children: {
|
||||
title: "Children",
|
||||
...linkedDataSchema,
|
||||
formControlId: "react-children",
|
||||
defaults: ["text"],
|
||||
examples: ["Foo"],
|
||||
},
|
||||
restrictedWithChildren: {
|
||||
title: "Restricted children with react defaults",
|
||||
...linkedDataSchema,
|
||||
ids: ["objects", "arrays"],
|
||||
defaults: ["text"],
|
||||
},
|
||||
restrictedChildrenWithReactDefaults: {
|
||||
title: "Restricted children without react defaults",
|
||||
...linkedDataSchema,
|
||||
ids: ["children"],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component interpreted by a plugin",
|
||||
description: "A test component's schema definition.",
|
||||
type: "string",
|
||||
id: "component-with-plugin-interpretation",
|
||||
pluginId: "string-plugin-resolver",
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with const",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "constKeyword",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Required with default",
|
||||
type: "string",
|
||||
const: "foo",
|
||||
default: "default",
|
||||
},
|
||||
bar: {
|
||||
title: "Required without default",
|
||||
type: "number",
|
||||
const: 40,
|
||||
},
|
||||
bat: {
|
||||
title: "Optional without default",
|
||||
type: "boolean",
|
||||
const: true,
|
||||
},
|
||||
},
|
||||
required: ["foo", "bar"],
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with custom CSS controls with overrides",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "controlPluginCssWithOverrides",
|
||||
properties: {
|
||||
cssWithOverrides: {
|
||||
title: "CSS with overrides",
|
||||
type: "string",
|
||||
formControlId: "custom-controls/css-with-overrides",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with custom CSS controls",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "controlPluginCss",
|
||||
properties: {
|
||||
css: {
|
||||
title: "CSS",
|
||||
type: "string",
|
||||
formControlId: "custom-controls/css",
|
||||
},
|
||||
object: {
|
||||
title: "Nested object",
|
||||
type: "object",
|
||||
properties: {
|
||||
cssWithOverrides2: {
|
||||
title: "CSS 2",
|
||||
type: "string",
|
||||
formControlId: "custom-controls/css",
|
||||
},
|
||||
},
|
||||
},
|
||||
children: {
|
||||
title: "Children",
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with custom controls",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "customControl",
|
||||
properties: {
|
||||
file: {
|
||||
title: "File",
|
||||
type: "string",
|
||||
formControlId: "custom-controls/file",
|
||||
},
|
||||
textAlign: {
|
||||
title: "Text align",
|
||||
type: "string",
|
||||
enum: ["left", "center", "right", "justify"],
|
||||
formControlId: "custom-controls/textAlign",
|
||||
},
|
||||
align: {
|
||||
title: "Align",
|
||||
type: "string",
|
||||
enum: ["top", "center", "bottom"],
|
||||
formControlId: "custom-controls/align",
|
||||
},
|
||||
fileUpload: {
|
||||
title: "File upload",
|
||||
type: "string",
|
||||
formControlId: "custom-controls/fileUpload",
|
||||
},
|
||||
theme: {
|
||||
title: "Theme",
|
||||
type: "string",
|
||||
enum: ["light", "dark"],
|
||||
formControlId: "custom-controls/theme",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with defaults",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "defaults",
|
||||
properties: {
|
||||
a: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
default: "correct",
|
||||
},
|
||||
b: {
|
||||
title: "Without default",
|
||||
type: "string",
|
||||
},
|
||||
c: {
|
||||
title: "With default",
|
||||
type: "object",
|
||||
properties: {
|
||||
alpha: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
default: "correct",
|
||||
},
|
||||
beta: {
|
||||
title: "Without default",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
alpha: "incorrect",
|
||||
beta: "correct",
|
||||
},
|
||||
},
|
||||
d: {
|
||||
title: "Without default",
|
||||
type: "object",
|
||||
properties: {
|
||||
alpha: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
default: "correct",
|
||||
},
|
||||
beta: {
|
||||
title: "Without default",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
e: {
|
||||
title: "With default",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
default: "correct",
|
||||
},
|
||||
default: ["correct", "correct"],
|
||||
},
|
||||
f: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
enum: ["foo", "bar"],
|
||||
default: "bar",
|
||||
},
|
||||
g: {
|
||||
title: "With default",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
h: {
|
||||
title: "With default",
|
||||
type: "string",
|
||||
default: "foo",
|
||||
const: "foo",
|
||||
},
|
||||
i: {
|
||||
title: "With default",
|
||||
type: "null",
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
default: {
|
||||
a: "incorrect",
|
||||
b: "correct",
|
||||
c: {
|
||||
alpha: "incorrect",
|
||||
beta: "incorrect",
|
||||
},
|
||||
d: {
|
||||
alpha: "incorrect",
|
||||
beta: "correct",
|
||||
},
|
||||
e: ["correct"],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with additional properties",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "dictionary",
|
||||
propertyTitle: "Item key",
|
||||
properties: {
|
||||
additionalObjects: {
|
||||
title: "A dictionary of objects",
|
||||
type: "object",
|
||||
additionalProperties: {
|
||||
title: "An object",
|
||||
type: "object",
|
||||
properties: {
|
||||
foo: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalFalse: {
|
||||
title: "A non-dictionary",
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: {
|
||||
title: "String item",
|
||||
type: "string",
|
||||
},
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with all disabled types",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "disabled",
|
||||
disabled: true,
|
||||
properties: {
|
||||
textarea: {
|
||||
title: "textarea",
|
||||
type: "string",
|
||||
disabled: true,
|
||||
},
|
||||
"section-link": {
|
||||
title: "section-link",
|
||||
type: "object",
|
||||
disabled: true,
|
||||
},
|
||||
display: {
|
||||
title: "display",
|
||||
const: "foobar",
|
||||
disabled: true,
|
||||
},
|
||||
checkbox: {
|
||||
title: "checkbox",
|
||||
type: "boolean",
|
||||
disabled: true,
|
||||
},
|
||||
button: {
|
||||
title: "button",
|
||||
type: "null",
|
||||
disabled: true,
|
||||
},
|
||||
array: {
|
||||
title: "array",
|
||||
items: {
|
||||
title: "array item",
|
||||
type: "string",
|
||||
disabled: true,
|
||||
},
|
||||
disabled: true,
|
||||
},
|
||||
"number-field": {
|
||||
title: "number-field",
|
||||
type: "number",
|
||||
disabled: true,
|
||||
},
|
||||
select: {
|
||||
title: "select",
|
||||
type: "string",
|
||||
enum: ["foo", "bar", "bat"],
|
||||
disabled: true,
|
||||
},
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "General example",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "generalExample",
|
||||
properties: {
|
||||
title: {
|
||||
title: "Title",
|
||||
type: "string",
|
||||
},
|
||||
toggle: {
|
||||
title: "Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
details: {
|
||||
title: "Details",
|
||||
type: "string",
|
||||
},
|
||||
toggle2: {
|
||||
title: "Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
tag: {
|
||||
title: "HTML tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
level: {
|
||||
title: "Level",
|
||||
type: "number",
|
||||
examples: [5],
|
||||
},
|
||||
text: {
|
||||
title: "Text",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
},
|
||||
alignHorizontal: {
|
||||
title: "Align horizontal",
|
||||
type: "string",
|
||||
enum: ["left", "center", "right"],
|
||||
},
|
||||
alignVertical: {
|
||||
title: "Align vertical",
|
||||
type: "string",
|
||||
enum: ["top", "bottom", "center"],
|
||||
},
|
||||
level2: {
|
||||
title: "Level",
|
||||
type: "number",
|
||||
examples: [100],
|
||||
},
|
||||
objectNoRequired: {
|
||||
title: "Object (no required items)",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
objectWithRequired: {
|
||||
title: "Object (with required items)",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: ["boolean"],
|
||||
},
|
||||
optionalObjectWithRequired: {
|
||||
title: "Optional object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
optionalObjectNoRequired: {
|
||||
title: "Object with no required items",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
strings: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
minItems: 0,
|
||||
maxItems: 6,
|
||||
},
|
||||
theme: {
|
||||
title: "Theme",
|
||||
type: "string",
|
||||
enum: ["light", "dark"],
|
||||
},
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"alignHorizontal",
|
||||
"alignVertical",
|
||||
"level",
|
||||
"level2",
|
||||
"title",
|
||||
"details",
|
||||
"tag",
|
||||
"toggle",
|
||||
"toggle2",
|
||||
],
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "General example",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "generalExample",
|
||||
properties: {
|
||||
title: {
|
||||
title: "Title",
|
||||
type: "string",
|
||||
},
|
||||
toggle: {
|
||||
title: "Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
details: {
|
||||
title: "Details",
|
||||
type: "string",
|
||||
},
|
||||
toggle2: {
|
||||
title: "Checkbox",
|
||||
type: "boolean",
|
||||
},
|
||||
tag: {
|
||||
title: "HTML tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
level: {
|
||||
title: "Level",
|
||||
type: "number",
|
||||
examples: [5],
|
||||
},
|
||||
text: {
|
||||
title: "Text",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
},
|
||||
alignHorizontal: {
|
||||
title: "Align horizontal",
|
||||
type: "string",
|
||||
enum: ["left", "center", "right"],
|
||||
},
|
||||
alignVertical: {
|
||||
title: "Align vertical",
|
||||
type: "string",
|
||||
enum: ["top", "bottom", "center"],
|
||||
},
|
||||
level2: {
|
||||
title: "Level",
|
||||
type: "number",
|
||||
examples: [100],
|
||||
},
|
||||
objectNoRequired: {
|
||||
title: "Object (no required items)",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
objectWithRequired: {
|
||||
title: "Object (with required items)",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: ["boolean"],
|
||||
},
|
||||
optionalObjectWithRequired: {
|
||||
title: "Optional object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
optionalObjectNoRequired: {
|
||||
title: "Object with no required items",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
strings: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
minItems: 0,
|
||||
maxItems: 6,
|
||||
},
|
||||
theme: {
|
||||
title: "Theme",
|
||||
type: "string",
|
||||
enum: ["light", "dark"],
|
||||
},
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
"alignHorizontal",
|
||||
"alignVertical",
|
||||
"level",
|
||||
"level2",
|
||||
"title",
|
||||
"details",
|
||||
"tag",
|
||||
"toggle",
|
||||
"toggle2",
|
||||
],
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
import alignHorizontalSchema from "./align-horizontal.schema";
|
||||
import allControlTypesSchema from "./all-control-types.schema";
|
||||
import anyOfSchema from "./any-of.schema";
|
||||
import arraysSchema from "./arrays.schema";
|
||||
import badgeSchema from "./badge.schema";
|
||||
import categorySchema from "./category.schema";
|
||||
import checkboxSchema from "./checkbox.schema";
|
||||
import childrenPluginSchema from "./children.schema";
|
||||
import childrenSchema from "./children.schema";
|
||||
import componentPluginSchema from "./component-plugin.schema";
|
||||
import constSchema from "./const.schema";
|
||||
import controlPluginSchema from "./control-plugin.schema";
|
||||
import controlPluginCssWithOverridesSchema from "./control-plugin.css-with-overrides.schema";
|
||||
import controlPluginCssSchema from "./control-plugin.css.schema";
|
||||
import defaultsSchema from "./defaults.schema";
|
||||
import dictionarySchema from "./dictionary.schema";
|
||||
import disabledSchema from "./disabled.schema";
|
||||
import generalExampleSchema from "./general-example.schema";
|
||||
import generalSchema from "./general.schema";
|
||||
import invalidDataSchema from "./invalid-data.schema";
|
||||
import mergedOneOfSchema from "./merged-one-of.schema";
|
||||
import nestedOneOfSchema from "./nested-one-of.schema";
|
||||
import nullSchema from "./null.schema";
|
||||
import numberFieldSchema from "./number-field.schema";
|
||||
import objectsSchema from "./objects.schema";
|
||||
import oneOfDeeplyNestedSchema from "./one-of-deeply-nested.schema";
|
||||
import oneOfSchema from "./one-of.schema";
|
||||
import textFieldSchema from "./text-field.schema";
|
||||
import textareaSchema from "./textarea.schema";
|
||||
import textSchema from "./text.schema";
|
||||
import tooltipSchema from "./tooltip.schema";
|
||||
|
||||
export {
|
||||
alignHorizontalSchema,
|
||||
allControlTypesSchema,
|
||||
anyOfSchema,
|
||||
arraysSchema,
|
||||
badgeSchema,
|
||||
categorySchema,
|
||||
checkboxSchema,
|
||||
childrenPluginSchema,
|
||||
childrenSchema,
|
||||
componentPluginSchema,
|
||||
constSchema,
|
||||
controlPluginSchema,
|
||||
controlPluginCssWithOverridesSchema,
|
||||
controlPluginCssSchema,
|
||||
defaultsSchema,
|
||||
dictionarySchema,
|
||||
disabledSchema,
|
||||
generalExampleSchema,
|
||||
generalSchema,
|
||||
invalidDataSchema,
|
||||
mergedOneOfSchema,
|
||||
nestedOneOfSchema,
|
||||
nullSchema,
|
||||
numberFieldSchema,
|
||||
objectsSchema,
|
||||
oneOfDeeplyNestedSchema,
|
||||
oneOfSchema,
|
||||
textFieldSchema,
|
||||
textareaSchema,
|
||||
textSchema,
|
||||
tooltipSchema,
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with invalid data",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "invalidData",
|
||||
properties: {
|
||||
invalidBooleanWrongType: {
|
||||
title: "Invalid boolean wrong type",
|
||||
type: "boolean",
|
||||
},
|
||||
invalidBooleanRequired: {
|
||||
title: "Invalid boolean required",
|
||||
type: "boolean",
|
||||
},
|
||||
invalidNullWrongType: {
|
||||
title: "Invalid null wrong type",
|
||||
type: "null",
|
||||
},
|
||||
invalidNullRequired: {
|
||||
title: "Invalid null required",
|
||||
type: "null",
|
||||
},
|
||||
invalidStringWrongType: {
|
||||
title: "Invalid string wrong type",
|
||||
type: "string",
|
||||
},
|
||||
invalidNumberWrongType: {
|
||||
title: "Invalid number wrong type",
|
||||
type: "number",
|
||||
},
|
||||
invalidEnumWrongType: {
|
||||
title: "Invalid enum wrong type",
|
||||
type: "string",
|
||||
enum: ["foo", "bar", "bat"],
|
||||
},
|
||||
invalidArrayWrongType: {
|
||||
title: "Invalid array wrong type",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Invalid array wrong type item",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
invalidObjectWrongType: {
|
||||
title: "Invalid object wrong type",
|
||||
type: "object",
|
||||
},
|
||||
invalidObjectMissingProperty: {
|
||||
title: "Invalid object missing property",
|
||||
type: "object",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "String",
|
||||
type: "string",
|
||||
},
|
||||
bar: {
|
||||
title: "Missing required property",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["bar"],
|
||||
},
|
||||
objectExample: {
|
||||
title: "Object",
|
||||
type: "object",
|
||||
properties: {
|
||||
invalidBooleanWrongType: {
|
||||
title: "Invalid boolean wrong type",
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayExample: {
|
||||
title: "Array",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Invalid string wrong type",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["invalidBooleanRequired", "invalidNullRequired"],
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
type: "object",
|
||||
id: "mergedOneOf",
|
||||
title: "Merged oneOf",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Foo",
|
||||
type: "object",
|
||||
required: ["a", "b"],
|
||||
oneOf: [
|
||||
{
|
||||
title: "Overridden Foo 1",
|
||||
description: "A is string",
|
||||
properties: {
|
||||
a: {
|
||||
title: "A",
|
||||
type: "string",
|
||||
},
|
||||
b: {
|
||||
title: "B",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Overridden Foo 2",
|
||||
description: "B is string",
|
||||
properties: {
|
||||
a: {
|
||||
title: "A",
|
||||
type: "number",
|
||||
},
|
||||
b: {
|
||||
title: "B",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with a nested oneOf",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "nestedOneOf",
|
||||
properties: {
|
||||
single: {
|
||||
title: "Single oneOf",
|
||||
type: "object",
|
||||
oneOf: [
|
||||
{
|
||||
title: "Object",
|
||||
description: "string",
|
||||
type: "object",
|
||||
properties: {
|
||||
omega: {
|
||||
title: "Omega",
|
||||
type: "string",
|
||||
},
|
||||
alpha: {
|
||||
title: "Alpha",
|
||||
type: "object",
|
||||
properties: {
|
||||
beta: {
|
||||
title: "string",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["omega", "alpha"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with null",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "nullKeyword",
|
||||
properties: {
|
||||
optionalNull: {
|
||||
title: "optional null",
|
||||
type: "null",
|
||||
},
|
||||
requiredNull: {
|
||||
title: "required null",
|
||||
type: "null",
|
||||
},
|
||||
},
|
||||
required: ["requiredNull"],
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with number-field",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "numberField",
|
||||
properties: {
|
||||
level: {
|
||||
title: "Level",
|
||||
type: "number",
|
||||
enum: [1, 2, 3, 4],
|
||||
default: 1,
|
||||
},
|
||||
quantity: {
|
||||
title: "Quantity",
|
||||
type: "number",
|
||||
examples: [10],
|
||||
minimum: 0,
|
||||
maximum: 50,
|
||||
multipleOf: 5,
|
||||
},
|
||||
defaultNumber: {
|
||||
title: "Default number",
|
||||
type: "number",
|
||||
default: 5,
|
||||
},
|
||||
optionalNumber: {
|
||||
title: "Optional number field",
|
||||
type: "number",
|
||||
},
|
||||
disabledNumber: {
|
||||
title: "Disabled number field",
|
||||
type: "number",
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
required: ["level", "quantity"],
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with objects",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "objects",
|
||||
properties: {
|
||||
objectNoRequired: {
|
||||
title: "object with no required items",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
objectWithRequired: {
|
||||
title: "object with required items",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
required: ["boolean"],
|
||||
},
|
||||
optionalObjectWithRequired: {
|
||||
title: "optional object",
|
||||
type: "object",
|
||||
properties: {
|
||||
string: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
optionalObjectNoRequired: {
|
||||
title: "object with no required items",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
optionalObjectWithNestedObject: {
|
||||
title: "object with nested object",
|
||||
type: "object",
|
||||
properties: {
|
||||
nestedObject: {
|
||||
title: "Nested object",
|
||||
type: "object",
|
||||
properties: {
|
||||
boolean: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optionalObjectWithDefaultValue: {
|
||||
title: "object with defaultValues",
|
||||
type: "object",
|
||||
properties: {
|
||||
foo: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
optionalObjectDisabled: {
|
||||
title: "object disabled",
|
||||
type: "object",
|
||||
disabled: true,
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Disabled textarea",
|
||||
type: "string",
|
||||
},
|
||||
bar: {
|
||||
title: "Disabled numberfield",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["objectNoRequired", "objectWithRequired"],
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Complex nesting arrays with oneOf",
|
||||
type: "object",
|
||||
id: "oneOfArrays",
|
||||
properties: {
|
||||
propertyKey: {
|
||||
title: "Property key",
|
||||
oneOf: [
|
||||
{
|
||||
title: "Containing object",
|
||||
type: "object",
|
||||
properties: {
|
||||
propertyKey1: {
|
||||
title: "propertyKey1 items",
|
||||
type: "object",
|
||||
properties: {
|
||||
propertyKey2: {
|
||||
title: "propertyKey2",
|
||||
type: "object",
|
||||
oneOf: [
|
||||
{
|
||||
title: "Alpha",
|
||||
type: "object",
|
||||
description: "Alpha",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Foo",
|
||||
type: "string",
|
||||
enum: ["a"],
|
||||
},
|
||||
bar: {
|
||||
title: "Bar",
|
||||
type: "string",
|
||||
enum: ["a"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Beta",
|
||||
type: "object",
|
||||
description: "Beta",
|
||||
properties: {
|
||||
foo: {
|
||||
title: "Foo",
|
||||
type: "string",
|
||||
enum: ["b"],
|
||||
},
|
||||
bar: {
|
||||
title: "Bar",
|
||||
type: "string",
|
||||
enum: ["b"],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["propertyKey1"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
import { linkedDataSchema } from "@microsoft/fast-tooling";
|
||||
|
||||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with oneOf",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "oneOf",
|
||||
oneOf: [
|
||||
{
|
||||
description: "string",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
string: {
|
||||
title: "string",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["string"],
|
||||
},
|
||||
{
|
||||
description: "number",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
number: {
|
||||
title: "number",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["number"],
|
||||
},
|
||||
{
|
||||
title: "Number or string",
|
||||
description: "number or string",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
numberOrString: {
|
||||
title: "Number or string configuration",
|
||||
oneOf: [
|
||||
{
|
||||
title: "number",
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
title: "string",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "object",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
object: {
|
||||
title: "An object",
|
||||
type: "object",
|
||||
properties: {
|
||||
number: {
|
||||
title: "number",
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
required: ["number"],
|
||||
},
|
||||
},
|
||||
required: ["object"],
|
||||
},
|
||||
{
|
||||
title: "array",
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Array",
|
||||
oneOf: [
|
||||
{
|
||||
title: "string item",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "number item",
|
||||
type: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "children object",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
required: ["numberOrString"],
|
||||
},
|
||||
{
|
||||
description: "category",
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
foo: {
|
||||
title: "string",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["foo"],
|
||||
formConfig: {
|
||||
categories: [
|
||||
{
|
||||
title: "Category A",
|
||||
expandable: true,
|
||||
items: ["foo"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with text-field",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "textField",
|
||||
properties: {
|
||||
tag: {
|
||||
title: "Tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
text: {
|
||||
title: "Text",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
},
|
||||
},
|
||||
required: ["tag", "text"],
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
description: "A test component's schema definition.",
|
||||
id: "text",
|
||||
title: "Text",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with text-field",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "textField",
|
||||
properties: {
|
||||
tag: {
|
||||
title: "Tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
textWithExamples: {
|
||||
title: "Text with examples",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
},
|
||||
textWithDefault: {
|
||||
title: "Text with default",
|
||||
type: "string",
|
||||
default: "Default value",
|
||||
},
|
||||
optionalTextWithExamples: {
|
||||
title: "Optional text with examples",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
},
|
||||
optionalTextWithDefault: {
|
||||
title: "Optional text with default",
|
||||
type: "string",
|
||||
default: "Default value",
|
||||
},
|
||||
optionalTag: {
|
||||
title: "Optional tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
},
|
||||
disabledTag: {
|
||||
title: "Disabled tag",
|
||||
type: "string",
|
||||
enum: ["span", "button"],
|
||||
default: "button",
|
||||
disabled: true,
|
||||
},
|
||||
disabledText: {
|
||||
title: "Disabled text area",
|
||||
type: "string",
|
||||
examples: ["Example text"],
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
required: ["tag", "textWithDefault", "textWithExamples", "disabledText"],
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
export default {
|
||||
$schema: "http://json-schema.org/schema#",
|
||||
title: "Component with tooltips",
|
||||
description: "A test component's schema definition.",
|
||||
type: "object",
|
||||
id: "tooltip",
|
||||
properties: {
|
||||
labelOnStandardControl: {
|
||||
title: "My label 1",
|
||||
description: "My label's tooltip 1",
|
||||
type: "string",
|
||||
},
|
||||
labelOnSingleLineControl: {
|
||||
title: "My label 2",
|
||||
description: "My label's tooltip 2",
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./mapping";
|
|
@ -0,0 +1,316 @@
|
|||
import React from "react";
|
||||
import "../__tests__/mocks/match-media";
|
||||
import {
|
||||
DataType,
|
||||
dictionaryLink,
|
||||
linkedDataSchema,
|
||||
mapDataDictionary,
|
||||
pluginIdKeyword,
|
||||
} from "@microsoft/fast-tooling";
|
||||
import { ComponentDictionary, reactMapper, reactResolver } from "./mapping";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import { configure, mount } from "enzyme";
|
||||
|
||||
/**
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
class Foo extends React.Component<{}, {}> {
|
||||
public render(): React.ReactNode {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class Bar extends React.Component<{}, {}> {
|
||||
public render(): React.ReactNode {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const componentDictionary: ComponentDictionary = {
|
||||
foo: Foo,
|
||||
bar: Bar,
|
||||
};
|
||||
|
||||
describe("reactMapper", () => {
|
||||
test("should map data to a React component as props", () => {
|
||||
const resolvedData: any = mapDataDictionary({
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: "foo",
|
||||
data: {
|
||||
text: "Hello",
|
||||
number: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
mapper: reactMapper(componentDictionary),
|
||||
resolver: reactResolver,
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
type: "string",
|
||||
},
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const mappedData: any = mount(resolvedData);
|
||||
const mappedComponent: any = mappedData.find("Foo");
|
||||
|
||||
expect(mappedComponent).toHaveLength(1);
|
||||
expect(mappedComponent.prop("text")).toEqual("Hello");
|
||||
expect(mappedComponent.prop("number")).toEqual(42);
|
||||
});
|
||||
test("should map data to a React component as children", () => {
|
||||
const resolvedData: any = mapDataDictionary({
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: "foo",
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bat",
|
||||
},
|
||||
{
|
||||
id: "bar",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
schemaId: "bar",
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
data: "Hello world",
|
||||
},
|
||||
bat: {
|
||||
schemaId: "bar",
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
data: "Foo",
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
mapper: reactMapper(componentDictionary),
|
||||
resolver: reactResolver,
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
});
|
||||
const mappedData: any = mount(resolvedData);
|
||||
|
||||
expect(mappedData.find("Foo")).toHaveLength(1);
|
||||
expect(mappedData.text()).toEqual("FooHello world");
|
||||
});
|
||||
test("should map data to nested React components", () => {
|
||||
const mappedData: any = mount(
|
||||
mapDataDictionary({
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: "foo",
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bar",
|
||||
dataLocation: "children",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
schemaId: "bar",
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bat",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bat: {
|
||||
schemaId: "bat",
|
||||
parent: {
|
||||
id: "bar",
|
||||
dataLocation: "children",
|
||||
},
|
||||
data: "Hello world",
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
mapper: reactMapper(componentDictionary),
|
||||
resolver: reactResolver,
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
bat: {
|
||||
id: "bat",
|
||||
type: DataType.string,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(mappedData.find("Foo")).toHaveLength(1);
|
||||
expect(mappedData.find("Bar")).toHaveLength(1);
|
||||
expect(mappedData.text()).toEqual("Hello world");
|
||||
});
|
||||
test("should map data with a plugin", () => {
|
||||
const pluginId: string = "foobarbat";
|
||||
function mapperPlugin(data: any): any {
|
||||
return "Hello world, " + data;
|
||||
}
|
||||
const resolvedData: any = mapDataDictionary({
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: "foo",
|
||||
data: {
|
||||
text: "!",
|
||||
number: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
mapper: reactMapper(componentDictionary),
|
||||
resolver: reactResolver,
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
[pluginIdKeyword]: pluginId,
|
||||
type: "string",
|
||||
},
|
||||
number: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
ids: [pluginId],
|
||||
mapper: mapperPlugin,
|
||||
resolver: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
const mappedData: any = mount(resolvedData);
|
||||
const mappedComponent: any = mappedData.find("Foo");
|
||||
|
||||
expect(mappedComponent).toHaveLength(1);
|
||||
expect(mappedComponent.prop("text")).toEqual("Hello world, !");
|
||||
expect(mappedComponent.prop("number")).toEqual(42);
|
||||
});
|
||||
test("should resolve data with a plugin", () => {
|
||||
const pluginId: string = "foobarbat";
|
||||
function resolverPlugin(data: any): any {
|
||||
return "Hello world";
|
||||
}
|
||||
const resolvedData: any = mapDataDictionary({
|
||||
dataDictionary: [
|
||||
{
|
||||
foo: {
|
||||
schemaId: "foo",
|
||||
data: {
|
||||
children: [
|
||||
{
|
||||
id: "bar",
|
||||
dataLocation: "children",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
schemaId: "foo",
|
||||
parent: {
|
||||
id: "foo",
|
||||
dataLocation: "children",
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
"foo",
|
||||
],
|
||||
mapper: reactMapper(componentDictionary),
|
||||
resolver: reactResolver,
|
||||
schemaDictionary: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
type: "object",
|
||||
properties: {
|
||||
children: {
|
||||
...linkedDataSchema,
|
||||
[pluginIdKeyword]: pluginId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
ids: [pluginId],
|
||||
mapper: undefined,
|
||||
resolver: resolverPlugin,
|
||||
},
|
||||
],
|
||||
});
|
||||
const mappedData: any = mount(resolvedData);
|
||||
const mappedComponent: any = mappedData.find("Foo");
|
||||
|
||||
expect(mappedComponent).toHaveLength(1);
|
||||
expect(mappedComponent.prop("children")).toEqual(["Hello world"]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,166 @@
|
|||
import React, { ComponentClass, FunctionComponent } from "react";
|
||||
import {
|
||||
dictionaryLink,
|
||||
MapperConfig,
|
||||
pluginIdKeyword,
|
||||
PropertyKeyword,
|
||||
ResolverConfig,
|
||||
} from "@microsoft/fast-tooling";
|
||||
|
||||
export interface ReactMapDataDictionaryPlugin {
|
||||
/**
|
||||
* The ids that map to the pluginIdKeyword in the JSON schema
|
||||
*/
|
||||
ids: string[];
|
||||
|
||||
/**
|
||||
* The mapping function that returns the mapped values
|
||||
*/
|
||||
mapper: (data: any) => any;
|
||||
|
||||
/**
|
||||
* The resolving function that returns the mapped values
|
||||
*/
|
||||
resolver: (data: any) => any;
|
||||
}
|
||||
|
||||
export interface ComponentDictionary {
|
||||
[key: string]: FunctionComponent<any> | ComponentClass<any> | string;
|
||||
}
|
||||
|
||||
function getPluginResolver(
|
||||
plugins: ReactMapDataDictionaryPlugin[],
|
||||
id?: string
|
||||
): ReactMapDataDictionaryPlugin | false {
|
||||
if (typeof id !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return plugins.find((plugin: ReactMapDataDictionaryPlugin) => {
|
||||
return plugin.ids.includes(id);
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePropertyWithPlugin(
|
||||
plugin: ReactMapDataDictionaryPlugin,
|
||||
value: any
|
||||
): any {
|
||||
return plugin.resolver(value);
|
||||
}
|
||||
|
||||
function mapPropertyWithPlugin(plugin: ReactMapDataDictionaryPlugin, value: any): any {
|
||||
return plugin.mapper(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping function intended to be used with the
|
||||
* `mapDataDictionary` export from the @microsoft/fast-tooling library
|
||||
*/
|
||||
export function reactMapper(
|
||||
componentDictionary: ComponentDictionary
|
||||
): (config: MapperConfig<JSX.Element>) => void {
|
||||
return (config: MapperConfig<JSX.Element>): void => {
|
||||
if (typeof config.dataDictionary[0][config.dictionaryId].data === "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const allAvailableProps = Object.keys(config.schema[PropertyKeyword.properties]);
|
||||
|
||||
config.dataDictionary[0][config.dictionaryId].data = {
|
||||
component: componentDictionary[config.schema.id],
|
||||
props: allAvailableProps
|
||||
.filter(potentialProp => {
|
||||
// remove slots from the attributes list
|
||||
return !allAvailableProps
|
||||
.filter((propName: string) => {
|
||||
if (
|
||||
config.schema[PropertyKeyword.properties][propName][
|
||||
dictionaryLink
|
||||
]
|
||||
) {
|
||||
return propName;
|
||||
}
|
||||
})
|
||||
.includes(potentialProp);
|
||||
})
|
||||
.reduce((previousValue: {}, currentValue: string) => {
|
||||
const plugin = getPluginResolver(
|
||||
config.mapperPlugins,
|
||||
config.schema[PropertyKeyword.properties][currentValue][
|
||||
pluginIdKeyword
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
...previousValue,
|
||||
[currentValue]: plugin
|
||||
? mapPropertyWithPlugin(
|
||||
plugin,
|
||||
config.dataDictionary[0][config.dictionaryId].data[
|
||||
currentValue
|
||||
]
|
||||
)
|
||||
: config.dataDictionary[0][config.dictionaryId].data[
|
||||
currentValue
|
||||
],
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A resolver function intended to be used with the
|
||||
* `mapDataDictionary` export from the @microsoft/fast-tooling library
|
||||
*/
|
||||
export function reactResolver(config: ResolverConfig<unknown>): any {
|
||||
if (config.dataDictionary[1] !== config.dictionaryId) {
|
||||
// the original data in the children location
|
||||
const childrenAtLocation =
|
||||
config.dataDictionary[0][
|
||||
config.dataDictionary[0][config.dictionaryId].parent.id
|
||||
].data.props[
|
||||
config.dataDictionary[0][config.dictionaryId].parent.dataLocation
|
||||
];
|
||||
const childrenProps: any = {
|
||||
...config.dataDictionary[0][config.dictionaryId].data.props,
|
||||
key: Array.isArray(childrenAtLocation) ? childrenAtLocation.length : 0,
|
||||
};
|
||||
const pluginId: string =
|
||||
config.schemaDictionary[
|
||||
config.dataDictionary[0][
|
||||
config.dataDictionary[0][config.dictionaryId].parent.id
|
||||
].schemaId
|
||||
][PropertyKeyword.properties][
|
||||
config.dataDictionary[0][config.dictionaryId].parent.dataLocation
|
||||
][pluginIdKeyword];
|
||||
const plugin = getPluginResolver(config.resolverPlugins, pluginId);
|
||||
|
||||
// the child item being resolved to a react component
|
||||
const newChildrenAtLocation = plugin
|
||||
? resolvePropertyWithPlugin(plugin, childrenProps)
|
||||
: typeof config.dataDictionary[0][config.dictionaryId].data === "string"
|
||||
? config.dataDictionary[0][config.dictionaryId].data
|
||||
: React.createElement(
|
||||
config.dataDictionary[0][config.dictionaryId].data.component,
|
||||
childrenProps
|
||||
);
|
||||
|
||||
// re-assign this prop with the new child item
|
||||
config.dataDictionary[0][
|
||||
config.dataDictionary[0][config.dictionaryId].parent.id
|
||||
].data.props[config.dataDictionary[0][config.dictionaryId].parent.dataLocation] =
|
||||
childrenAtLocation === undefined
|
||||
? [newChildrenAtLocation]
|
||||
: [newChildrenAtLocation, ...childrenAtLocation];
|
||||
}
|
||||
|
||||
if (typeof config.dataDictionary[0][config.dictionaryId].data === "string") {
|
||||
return config.dataDictionary[0][config.dictionaryId].data;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
config.dataDictionary[0][config.dictionaryId].data.component,
|
||||
config.dataDictionary[0][config.dictionaryId].data.props
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { ArrayControlConfig, DragState } from "../templates";
|
||||
|
||||
export type ArrayControlProps = ArrayControlConfig;
|
||||
|
||||
export type ArrayControlState = DragState;
|
|
@ -0,0 +1,429 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount } from "enzyme";
|
||||
import ArrayControlStyled, { ArrayControl } from "./control.array";
|
||||
import { ArrayControlProps } from "./control.array.props";
|
||||
import { ArrayControlClassNameContract } from "./control.array.style";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { ControlType } from "../templates";
|
||||
import { DataType, ValidationError } from "@microsoft/fast-tooling";
|
||||
import defaultStrings from "../form.strings";
|
||||
|
||||
const TestArrayControl: React.FC<any> = (
|
||||
props: React.PropsWithChildren<any>
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ArrayControl {...props} />
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const arrayProps: ArrayControlProps = {
|
||||
type: ControlType.array,
|
||||
dataLocation: "",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
navigation: {
|
||||
"": {
|
||||
self: "",
|
||||
parent: null,
|
||||
relativeDataLocation: "",
|
||||
schemaLocation: "",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.array,
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
schemaLocation: "",
|
||||
value: "",
|
||||
schema: {},
|
||||
minItems: 0,
|
||||
maxItems: Infinity,
|
||||
onChange: jest.fn(),
|
||||
onAddExampleData: jest.fn(),
|
||||
onUpdateSection: jest.fn(),
|
||||
reportValidity: jest.fn(),
|
||||
disabled: false,
|
||||
elementRef: null,
|
||||
updateValidity: jest.fn(),
|
||||
validationErrors: [],
|
||||
required: false,
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
const managedClasses: ArrayControlClassNameContract = {
|
||||
arrayControl: "arrayControl-class",
|
||||
arrayControl__disabled: "arrayControl__disabled-class",
|
||||
arrayControl__invalid: "arrayControl__disabled-class",
|
||||
arrayControl_invalidMessage: "arrayControl_invalidMessage-class",
|
||||
arrayControl_existingItemListItem__invalid:
|
||||
"arrayControl_existingItemListItem__invalid-class",
|
||||
arrayControl_addItem: "arrayControl_addItem-class",
|
||||
arrayControl_addItemLabel: "arrayControl_addItemLabel-class",
|
||||
arrayControl_addItemButton: "arrayControl_addItemButton-class",
|
||||
arrayControl_existingItemList: "arrayControl_existingItemList-class",
|
||||
arrayControl_existingItemListItem: "arrayControl_existingItemListItem-class",
|
||||
arrayControl_existingItemListItemLink: "arrayControl_existingItemListItemLink-class",
|
||||
arrayControl_existingItemListItemLink__default:
|
||||
"arrayControl_existingItemListItemLink__default-class",
|
||||
arrayControl_existingItemRemoveButton: "arrayControl_existingItemRemoveButton-class",
|
||||
};
|
||||
|
||||
describe("ArrayControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
mount(<ArrayControlStyled {...arrayProps} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate a button to add an array item if the maximum number of items has not been reached", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
maxItems={2}
|
||||
value={["foo"]}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
|
||||
).toEqual(1);
|
||||
});
|
||||
test("should generate a button to add an array item if no maximum number of items has been specified", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo"]}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
|
||||
).toEqual(1);
|
||||
});
|
||||
test("should not generate a button to add an array item if the maximum number of items has been reached", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
maxItems={2}
|
||||
value={["foo", "bar"]}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_addItemButton}`).length
|
||||
).toEqual(0);
|
||||
});
|
||||
test("should add an item to the array if the add button has been clicked", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo"]}
|
||||
managedClasses={managedClasses}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
const addButton: any = rendered.find(
|
||||
`.${managedClasses.arrayControl_addItemButton}`
|
||||
);
|
||||
addButton.simulate("click");
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toEqual({ value: void 0, isArray: true });
|
||||
});
|
||||
test("should show a remove button on an existing array item if the minimum number of items has not been reached", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo", "bar", "bat"]}
|
||||
managedClasses={managedClasses}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
|
||||
.length
|
||||
).toBe(3);
|
||||
});
|
||||
test("should show a remove button on an existing array item if the minimum number of items has not been specified", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo", "bar"]}
|
||||
managedClasses={managedClasses}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
|
||||
.length
|
||||
).toBe(2);
|
||||
});
|
||||
test("should not show a remove button on existing array items if the minimum number of items has been reached", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo", "bar"]}
|
||||
minItems={2}
|
||||
managedClasses={managedClasses}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
|
||||
.length
|
||||
).toBe(0);
|
||||
});
|
||||
test("should remove an array item if the remove button has been clicked", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={["foo", "bar", "bat"]}
|
||||
managedClasses={managedClasses}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
const removeButton: any = rendered
|
||||
.find(`.${managedClasses.arrayControl_existingItemRemoveButton}`)
|
||||
.at(1);
|
||||
removeButton.simulate("click");
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toEqual({
|
||||
value: undefined,
|
||||
isArray: true,
|
||||
index: 1,
|
||||
});
|
||||
});
|
||||
test("should not show default values if data exists", () => {
|
||||
const arrayItem1: string = "foo";
|
||||
const arrayItem2: string = "bar";
|
||||
const defaultArrayItem1: string = "hello";
|
||||
const defaultArrayItem2: string = "world";
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={[arrayItem1, arrayItem2]}
|
||||
default={[defaultArrayItem1, defaultArrayItem2]}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
expect(rendered.html().includes(arrayItem1)).toBe(true);
|
||||
expect(rendered.html().includes(arrayItem2)).toBe(true);
|
||||
expect(rendered.html().includes(defaultArrayItem1)).toBe(false);
|
||||
expect(rendered.html().includes(defaultArrayItem2)).toBe(false);
|
||||
});
|
||||
test("should fire an `onSectionUpdate` callback with a link is clicked", () => {
|
||||
const handleSectionUpdate: any = jest.fn();
|
||||
const arrayItem1: string = "foo";
|
||||
const arrayItem2: string = "bar";
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={[arrayItem1, arrayItem2]}
|
||||
managedClasses={managedClasses}
|
||||
onUpdateSection={handleSectionUpdate}
|
||||
navigationConfigId={""}
|
||||
navigation={{
|
||||
"": {
|
||||
self: "",
|
||||
parent: null,
|
||||
relativeDataLocation: "",
|
||||
schemaLocation: "",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.array,
|
||||
items: ["[0]", "[1]"],
|
||||
},
|
||||
"[0]": {
|
||||
self: "[0]",
|
||||
parent: "",
|
||||
relativeDataLocation: "[0]",
|
||||
schemaLocation: "items",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.string,
|
||||
items: [],
|
||||
},
|
||||
"[1]": {
|
||||
self: "[1]",
|
||||
parent: "",
|
||||
relativeDataLocation: "[1]",
|
||||
schemaLocation: "items",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "bar",
|
||||
type: DataType.string,
|
||||
items: [],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered.find("a").at(0).simulate("click");
|
||||
|
||||
expect(handleSectionUpdate).toHaveBeenCalled();
|
||||
expect(handleSectionUpdate.mock.calls[0][1]).toEqual("[0]");
|
||||
|
||||
const handleSectionUpdateWithTestLocations: any = jest.fn();
|
||||
const renderedWithTestLocations: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
value={[arrayItem1, arrayItem2]}
|
||||
managedClasses={managedClasses}
|
||||
onUpdateSection={handleSectionUpdateWithTestLocations}
|
||||
schemaLocation={"properties.test"}
|
||||
dataLocation={"test"}
|
||||
navigationConfigId={"test"}
|
||||
navigation={{
|
||||
"": {
|
||||
self: "",
|
||||
parent: null,
|
||||
relativeDataLocation: "",
|
||||
schemaLocation: "",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.object,
|
||||
items: ["test"],
|
||||
},
|
||||
test: {
|
||||
self: "test",
|
||||
parent: "",
|
||||
relativeDataLocation: "test",
|
||||
schemaLocation: "properties.test",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.array,
|
||||
items: ["test[0]", "test[1]"],
|
||||
},
|
||||
"test[0]": {
|
||||
self: "test[0]",
|
||||
parent: "test",
|
||||
relativeDataLocation: "test[0]",
|
||||
schemaLocation: "items",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "foo",
|
||||
type: DataType.string,
|
||||
items: [],
|
||||
},
|
||||
"test[1]": {
|
||||
self: "test[1]",
|
||||
parent: "test",
|
||||
relativeDataLocation: "test[1]",
|
||||
schemaLocation: "items",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
data: void 0,
|
||||
text: "bar",
|
||||
type: DataType.string,
|
||||
items: [],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
renderedWithTestLocations.find("a").at(0).simulate("click");
|
||||
|
||||
expect(handleSectionUpdateWithTestLocations).toHaveBeenCalled();
|
||||
expect(handleSectionUpdateWithTestLocations.mock.calls[0][1]).toEqual("test[0]");
|
||||
});
|
||||
test("should add a disabled class if the disabled prop has been passed", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
disabled={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.arrayControl__disabled}`)).toBeTruthy();
|
||||
});
|
||||
test("should add an invalid class if the invalidMessage prop is not an empty string", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
invalidMessage={"foo"}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.arrayControl__invalid}`)).toBeTruthy();
|
||||
});
|
||||
test("should add an invalid class on each array item that is invalid", () => {
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
invalidMessage={"foo"}
|
||||
validationErrors={[
|
||||
{ dataLocation: "foo.0", invalidMessage: "bar" } as ValidationError,
|
||||
]}
|
||||
managedClasses={managedClasses}
|
||||
dataLocation={"foo"}
|
||||
value={["foo", true]}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.arrayControl_existingItemListItem__invalid}`)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
test("should show an invalid message on each invalid array item if the displayInlineValidation prop is passed", () => {
|
||||
const invalidItemMessage: string = "foobarbat";
|
||||
const rendered: any = mount(
|
||||
<TestArrayControl
|
||||
{...arrayProps}
|
||||
invalidMessage={"foo"}
|
||||
validationErrors={[
|
||||
{
|
||||
dataLocation: "foo.0",
|
||||
invalidMessage: invalidItemMessage,
|
||||
} as ValidationError,
|
||||
]}
|
||||
displayValidationInline={true}
|
||||
managedClasses={managedClasses}
|
||||
dataLocation={"foo"}
|
||||
value={["foo", true]}
|
||||
/>
|
||||
);
|
||||
|
||||
const invalidItem: any = rendered.find(
|
||||
`.${managedClasses.arrayControl_invalidMessage}`
|
||||
);
|
||||
|
||||
expect(invalidItem.text()).toEqual(invalidItemMessage);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
import { ellipsis } from "@microsoft/fast-jss-utilities";
|
||||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import {
|
||||
addItemStyle,
|
||||
borderRadiusCSSProperty,
|
||||
cleanListStyle,
|
||||
defaultFontStyle,
|
||||
errorColorCSSProperty,
|
||||
gutterCSSProperty,
|
||||
invalidMessageStyle,
|
||||
L1CSSProperty,
|
||||
labelRegionStyle,
|
||||
labelStyle,
|
||||
removeItemStyle,
|
||||
} from "../../style";
|
||||
|
||||
/**
|
||||
* Array class name contract
|
||||
*/
|
||||
export interface ArrayControlClassNameContract {
|
||||
arrayControl?: string;
|
||||
arrayControl__disabled?: string;
|
||||
arrayControl__invalid?: string;
|
||||
arrayControl_invalidMessage?: string;
|
||||
arrayControl_addItem?: string;
|
||||
arrayControl_addItemButton?: string;
|
||||
arrayControl_addItemLabel?: string;
|
||||
arrayControl_existingItemList?: string;
|
||||
arrayControl_existingItemListItem?: string;
|
||||
arrayControl_existingItemListItem__invalid?: string;
|
||||
arrayControl_existingItemListItemLink?: string;
|
||||
arrayControl_existingItemListItemLink__default?: string;
|
||||
arrayControl_existingItemRemoveButton?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<ArrayControlClassNameContract, {}> = {
|
||||
arrayControl: {},
|
||||
arrayControl__disabled: {},
|
||||
arrayControl__invalid: {
|
||||
"& $arrayControl_existingItemList": {
|
||||
"&::before": {
|
||||
"border-color": errorColorCSSProperty,
|
||||
},
|
||||
},
|
||||
},
|
||||
arrayControl_invalidMessage: {
|
||||
...invalidMessageStyle,
|
||||
"padding-bottom": "5px",
|
||||
"margin-top": "-5px",
|
||||
},
|
||||
arrayControl_addItem: {
|
||||
...labelRegionStyle,
|
||||
position: "relative",
|
||||
},
|
||||
arrayControl_addItemLabel: {
|
||||
...labelStyle,
|
||||
"max-width": "calc(100% - 30px)",
|
||||
},
|
||||
arrayControl_addItemButton: {
|
||||
...addItemStyle,
|
||||
},
|
||||
arrayControl_existingItemList: {
|
||||
...cleanListStyle,
|
||||
position: "relative",
|
||||
"&::before": {
|
||||
content: "''",
|
||||
display: "block",
|
||||
width: "100%",
|
||||
"border-bottom": "1px solid transparent",
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
bottom: "0",
|
||||
},
|
||||
},
|
||||
arrayControl_existingItemListItem: {
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
height: "30px",
|
||||
"line-height": "30px",
|
||||
paddingBottom: "5px",
|
||||
"border-radius": borderRadiusCSSProperty,
|
||||
"&::before": {
|
||||
content: "''",
|
||||
position: "absolute",
|
||||
"border-radius": borderRadiusCSSProperty,
|
||||
"pointer-events": "none",
|
||||
height: "inherit",
|
||||
width: `calc(100% - calc(${gutterCSSProperty} + 6px))`,
|
||||
border: "1px solid transparent",
|
||||
},
|
||||
},
|
||||
arrayControl_existingItemListItem__invalid: {
|
||||
"&::before": {
|
||||
"border-color": errorColorCSSProperty,
|
||||
"box-sizing": "border-box",
|
||||
},
|
||||
},
|
||||
arrayControl_existingItemListItemLink: {
|
||||
...ellipsis(),
|
||||
cursor: "pointer",
|
||||
display: "block",
|
||||
height: "30px",
|
||||
"line-height": "30px",
|
||||
width: "calc(100% - 40px)",
|
||||
padding: "0 5px",
|
||||
"background-color": L1CSSProperty,
|
||||
"&$arrayControl_existingItemListItemLink__default": {
|
||||
...defaultFontStyle,
|
||||
cursor: "auto",
|
||||
},
|
||||
},
|
||||
arrayControl_existingItemListItemLink__default: {},
|
||||
arrayControl_existingItemRemoveButton: {
|
||||
...removeItemStyle,
|
||||
cursor: "pointer",
|
||||
},
|
||||
};
|
||||
|
||||
export default styles;
|
|
@ -0,0 +1,326 @@
|
|||
import { uniqueId } from "lodash-es";
|
||||
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
|
||||
import React from "react";
|
||||
import { getArrayLinks, isRootLocation } from "./utilities/form";
|
||||
import styles, { ArrayControlClassNameContract } from "./control.array.style";
|
||||
import { ArrayControlProps, ArrayControlState } from "./control.array.props";
|
||||
import { DragItem } from "../templates";
|
||||
import { ArrayAction } from "../templates/types";
|
||||
import { classNames } from "@microsoft/fast-web-utilities";
|
||||
|
||||
/**
|
||||
* Form control definition
|
||||
*/
|
||||
class ArrayControl extends React.Component<
|
||||
ArrayControlProps & ManagedClasses<ArrayControlClassNameContract>,
|
||||
ArrayControlState
|
||||
> {
|
||||
public static displayName: string = "ArrayControl";
|
||||
|
||||
public static defaultProps: Partial<
|
||||
ArrayControlProps & ManagedClasses<ArrayControlClassNameContract>
|
||||
> = {
|
||||
managedClasses: {},
|
||||
};
|
||||
|
||||
constructor(props: ArrayControlProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: props.value,
|
||||
isDragging: false,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
this.props.managedClasses.arrayControl,
|
||||
[
|
||||
this.props.managedClasses.arrayControl__disabled,
|
||||
this.props.disabled,
|
||||
],
|
||||
[
|
||||
this.props.managedClasses.arrayControl__invalid,
|
||||
this.props.invalidMessage !== "",
|
||||
]
|
||||
)}
|
||||
>
|
||||
{this.renderAddArrayItem()}
|
||||
{this.renderExistingArrayItems()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a button for adding an item to the array
|
||||
*/
|
||||
private renderAddArrayItemTrigger(): React.ReactNode {
|
||||
return (
|
||||
<button
|
||||
className={this.props.managedClasses.arrayControl_addItemButton}
|
||||
aria-label={this.props.strings.arrayAddItemTip}
|
||||
onClick={this.arrayItemClickHandlerFactory(ArrayAction.add)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an add array item section
|
||||
*/
|
||||
private renderAddArrayItem(): React.ReactNode {
|
||||
const existingItemLength: number = Array.isArray(this.props.value)
|
||||
? this.props.value.length
|
||||
: 0;
|
||||
const {
|
||||
arrayControl_addItem,
|
||||
arrayControl_addItemLabel,
|
||||
}: ArrayControlClassNameContract = this.props.managedClasses;
|
||||
|
||||
if (this.props.maxItems > existingItemLength) {
|
||||
return (
|
||||
<div className={arrayControl_addItem}>
|
||||
<div className={arrayControl_addItemLabel}>
|
||||
{this.props.strings.arrayAddItemLabel}
|
||||
</div>
|
||||
{this.renderAddArrayItemTrigger()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an default array link item
|
||||
*/
|
||||
private renderDefaultArrayLinkItem = (value: any, index: number): React.ReactNode => {
|
||||
const {
|
||||
arrayControl_existingItemListItem,
|
||||
arrayControl_existingItemListItemLink,
|
||||
arrayControl_existingItemListItemLink__default,
|
||||
}: ArrayControlClassNameContract = this.props.managedClasses;
|
||||
|
||||
return (
|
||||
<li
|
||||
className={arrayControl_existingItemListItem}
|
||||
key={`item-${index}`}
|
||||
id={uniqueId(index.toString())}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
arrayControl_existingItemListItemLink,
|
||||
arrayControl_existingItemListItemLink__default
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders default array items
|
||||
*/
|
||||
private renderDefaultArrayLinkItems(): React.ReactNode {
|
||||
return getArrayLinks(this.props.default).map(
|
||||
(value: any, index: number): React.ReactNode => {
|
||||
return this.renderDefaultArrayLinkItem(value, index);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the links to an array section to be activated
|
||||
*/
|
||||
private renderExistingArrayItems(): React.ReactNode {
|
||||
const hasData: boolean = Array.isArray(this.props.value);
|
||||
const hasDefault: boolean = Array.isArray(this.props.default);
|
||||
|
||||
if (hasData) {
|
||||
return (
|
||||
<ul className={this.props.managedClasses.arrayControl_existingItemList}>
|
||||
{this.renderArrayLinkItems()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDefault) {
|
||||
return (
|
||||
<ul className={this.props.managedClasses.arrayControl_existingItemList}>
|
||||
{this.renderDefaultArrayLinkItems()}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={this.props.managedClasses.arrayControl_existingItemList}></ul>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI for all items in an array
|
||||
*/
|
||||
private renderArrayLinkItems(): React.ReactNode {
|
||||
const data: unknown[] = this.state.isDragging
|
||||
? this.state.data
|
||||
: this.props.value;
|
||||
const {
|
||||
arrayControl_existingItemRemoveButton,
|
||||
arrayControl_existingItemListItem,
|
||||
arrayControl_existingItemListItem__invalid,
|
||||
arrayControl_existingItemListItemLink,
|
||||
}: Partial<ArrayControlClassNameContract> = this.props.managedClasses;
|
||||
|
||||
return getArrayLinks(data).map(
|
||||
(text: string, index: number): React.ReactNode => {
|
||||
const invalidError: React.ReactNode = this.renderValidationError(index);
|
||||
|
||||
return (
|
||||
<React.Fragment key={this.props.dataLocation + index}>
|
||||
<DragItem
|
||||
key={index}
|
||||
index={index}
|
||||
minItems={this.props.minItems}
|
||||
itemLength={getArrayLinks(data).length}
|
||||
itemRemoveClassName={arrayControl_existingItemRemoveButton}
|
||||
itemClassName={classNames(arrayControl_existingItemListItem, [
|
||||
arrayControl_existingItemListItem__invalid,
|
||||
invalidError !== null,
|
||||
])}
|
||||
itemLinkClassName={arrayControl_existingItemListItemLink}
|
||||
removeDragItem={this.arrayItemClickHandlerFactory}
|
||||
onClick={this.arrayClickHandlerFactory}
|
||||
moveDragItem={this.handleMoveDragItem}
|
||||
dropDragItem={this.handleDropDragItem}
|
||||
dragStart={this.handleDragStart}
|
||||
dragEnd={this.handleDragEnd}
|
||||
strings={this.props.strings}
|
||||
>
|
||||
{text}
|
||||
</DragItem>
|
||||
{!!this.props.displayValidationInline ? invalidError : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private renderValidationError(index: number): React.ReactNode {
|
||||
if (typeof this.props.validationErrors === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const error of this.props.validationErrors) {
|
||||
if (error.dataLocation.startsWith(`${this.props.dataLocation}.${index}`)) {
|
||||
return (
|
||||
<div
|
||||
className={this.props.managedClasses.arrayControl_invalidMessage}
|
||||
>
|
||||
{error.invalidMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array add/remove item click handler factory
|
||||
*/
|
||||
private arrayItemClickHandlerFactory = (
|
||||
type: ArrayAction,
|
||||
index?: number
|
||||
): ((e: React.MouseEvent<HTMLButtonElement>) => void) => {
|
||||
return (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault();
|
||||
|
||||
type === ArrayAction.add
|
||||
? this.handleAddArrayItem()
|
||||
: this.handleRemoveArrayItem(index);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Array section link click handler factory
|
||||
*/
|
||||
private arrayClickHandlerFactory = (
|
||||
index: number
|
||||
): ((e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void) => {
|
||||
return (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>): void => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onUpdateSection(
|
||||
this.props.dictionaryId,
|
||||
this.props.navigation[this.props.navigationConfigId].items[index]
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles adding an array item
|
||||
*/
|
||||
private handleAddArrayItem(): void {
|
||||
if (typeof this.props.value === "undefined") {
|
||||
this.props.onChange({ value: [this.props.onAddExampleData("items")] });
|
||||
} else {
|
||||
this.props.onChange({
|
||||
value: this.props.onAddExampleData("items"),
|
||||
isArray: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles removing an array item
|
||||
*/
|
||||
private handleRemoveArrayItem(index: number): void {
|
||||
this.props.onChange({ value: void 0, isArray: true, index });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the start of the drag action
|
||||
*/
|
||||
private handleDragStart = (): void => {
|
||||
this.setState({
|
||||
isDragging: true,
|
||||
data: [].concat(this.props.value || []),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the end of the drag action
|
||||
*/
|
||||
private handleDragEnd = (): void => {
|
||||
this.setState({
|
||||
isDragging: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle moving the drag item
|
||||
*/
|
||||
private handleMoveDragItem = (sourceIndex: number, targetIndex: number): void => {
|
||||
const currentData: unknown[] = [].concat(this.props.value);
|
||||
|
||||
if (sourceIndex !== targetIndex) {
|
||||
currentData.splice(targetIndex, 0, currentData.splice(sourceIndex, 1)[0]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
data: currentData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle dropping the drag item
|
||||
* Triggers the onChange
|
||||
*/
|
||||
private handleDropDragItem = (): void => {
|
||||
this.props.onChange({ value: this.state.data });
|
||||
};
|
||||
}
|
||||
|
||||
export { ArrayControl };
|
||||
export default manageJss(styles)(ArrayControl);
|
|
@ -0,0 +1,3 @@
|
|||
import { CommonControlConfig } from "../templates";
|
||||
|
||||
export type ButtonControlProps = CommonControlConfig;
|
|
@ -0,0 +1,116 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount, shallow } from "enzyme";
|
||||
import { ButtonControl } from "./control.button";
|
||||
import { ButtonControlProps } from "./control.button.props";
|
||||
import { ButtonControlClassNameContract } from "./control.button.style";
|
||||
import { ControlType } from "../templates";
|
||||
import defaultStrings from "../form.strings";
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const managedClasses: ButtonControlClassNameContract = {
|
||||
buttonControl: "buttonControl-class",
|
||||
buttonControl__disabled: "buttonControl__disabled-class",
|
||||
buttonControl__default: "buttonControl__default-class",
|
||||
};
|
||||
|
||||
const buttonProps: ButtonControlProps = {
|
||||
type: ControlType.button,
|
||||
dataLocation: "",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
navigation: {},
|
||||
onChange: jest.fn(),
|
||||
value: "",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
reportValidity: jest.fn(),
|
||||
updateValidity: jest.fn(),
|
||||
elementRef: null,
|
||||
validationErrors: [],
|
||||
required: false,
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
describe("ButtonControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
shallow(<ButtonControl {...buttonProps} managedClasses={managedClasses} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate an HTML button element", () => {
|
||||
const rendered: any = mount(
|
||||
<ButtonControl {...buttonProps} managedClasses={managedClasses} />
|
||||
);
|
||||
|
||||
expect(rendered.find("button")).toHaveLength(1);
|
||||
});
|
||||
test("should be disabled when disabled props is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<ButtonControl
|
||||
{...buttonProps}
|
||||
disabled={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.buttonControl__disabled}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
expect(rendered.find("button").prop("disabled")).toBeTruthy();
|
||||
expect(rendered.find("input").prop("disabled")).toBeTruthy();
|
||||
});
|
||||
test("should have the default class when default prop is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<ButtonControl
|
||||
{...buttonProps}
|
||||
value={undefined}
|
||||
default={null}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.buttonControl__default}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
});
|
||||
test("should fire the onChange event when the button is clicked", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<ButtonControl
|
||||
{...buttonProps}
|
||||
managedClasses={managedClasses}
|
||||
value={undefined}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
|
||||
rendered.find(`.${managedClasses.buttonControl}`).simulate("click");
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback.mock.calls[0][0]).toEqual({ value: null });
|
||||
});
|
||||
test("should NOT fire an onChange when the input value is updated", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<ButtonControl
|
||||
{...buttonProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered.find("input").simulate("change", { target: { value: "foo" } });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { defaultFontStyle, inputStyle } from "../../style";
|
||||
|
||||
/**
|
||||
* Button class name contract
|
||||
*/
|
||||
export interface ButtonControlClassNameContract {
|
||||
buttonControl?: string;
|
||||
buttonControl__disabled?: string;
|
||||
buttonControl__default?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<ButtonControlClassNameContract, {}> = {
|
||||
buttonControl: {
|
||||
...inputStyle,
|
||||
width: "100%",
|
||||
textAlign: "start",
|
||||
"&$buttonControl__default": {
|
||||
...defaultFontStyle,
|
||||
},
|
||||
},
|
||||
buttonControl__disabled: {},
|
||||
buttonControl__default: {},
|
||||
};
|
||||
|
||||
export default styles;
|
|
@ -0,0 +1,81 @@
|
|||
import React from "react";
|
||||
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
|
||||
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import styles, { ButtonControlClassNameContract } from "./control.button.style";
|
||||
import { ButtonControlProps } from "./control.button.props";
|
||||
import { classNames } from "@microsoft/fast-web-utilities";
|
||||
import { isDefault } from "./utilities/form";
|
||||
|
||||
/**
|
||||
* Form control definition
|
||||
* @extends React.Component
|
||||
*/
|
||||
class ButtonControl extends React.Component<
|
||||
ButtonControlProps & ManagedClasses<ButtonControlClassNameContract>,
|
||||
{}
|
||||
> {
|
||||
public static displayName: string = "ButtonControl";
|
||||
|
||||
public static defaultProps: Partial<
|
||||
ButtonControlProps & ManagedClasses<ButtonControlClassNameContract>
|
||||
> = {
|
||||
managedClasses: {},
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<button
|
||||
className={classNames(
|
||||
this.props.managedClasses.buttonControl,
|
||||
[
|
||||
this.props.managedClasses.buttonControl__disabled,
|
||||
this.props.disabled,
|
||||
],
|
||||
[
|
||||
this.props.managedClasses.buttonControl__default,
|
||||
isDefault(this.props.value, this.props.default),
|
||||
]
|
||||
)}
|
||||
ref={this.props.elementRef as React.Ref<HTMLButtonElement>}
|
||||
onBlur={this.props.updateValidity}
|
||||
onFocus={this.props.reportValidity}
|
||||
onClick={this.handleButtonClick()}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
Set to null
|
||||
</button>
|
||||
<input
|
||||
id={this.props.dataLocation}
|
||||
hidden={true}
|
||||
value={this.props.value || ""}
|
||||
onChange={this.handleInputChange}
|
||||
disabled={this.props.disabled}
|
||||
required={this.props.required}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private handleButtonClick = (): ((
|
||||
e: React.MouseEvent<HTMLButtonElement>
|
||||
) => void) => {
|
||||
return (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onChange({ value: null });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Dummy onChange handler
|
||||
*
|
||||
* Form elements will not validate against read-only form items
|
||||
* therefore a value and onChange handler must still be supplied
|
||||
* even if there is no intention to update the value.
|
||||
*/
|
||||
private handleInputChange = (): void => {};
|
||||
}
|
||||
|
||||
export { ButtonControl };
|
||||
export default manageJss(styles)(ButtonControl);
|
|
@ -0,0 +1,3 @@
|
|||
import { CommonControlConfig } from "../templates";
|
||||
|
||||
export type CheckboxControlProps = CommonControlConfig;
|
|
@ -0,0 +1,143 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount, shallow } from "enzyme";
|
||||
import { CheckboxControl } from "./control.checkbox";
|
||||
import { CheckboxControlProps } from "./control.checkbox.props";
|
||||
import { CheckboxControlClassNameContract } from "./control.checkbox.style";
|
||||
import { ControlType } from "../templates";
|
||||
import defaultStrings from "../form.strings";
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const managedClasses: CheckboxControlClassNameContract = {
|
||||
checkboxControl: "checkboxControl-class",
|
||||
checkboxControl__disabled: "checkboxControl__disabled-class",
|
||||
checkboxControl__default: "checkboxControl__default-class",
|
||||
};
|
||||
|
||||
const checkboxProps: CheckboxControlProps = {
|
||||
type: ControlType.checkbox,
|
||||
dataLocation: "",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
navigation: {},
|
||||
onChange: jest.fn(),
|
||||
value: false,
|
||||
schema: {},
|
||||
disabled: false,
|
||||
elementRef: null,
|
||||
reportValidity: jest.fn(),
|
||||
updateValidity: jest.fn(),
|
||||
validationErrors: [],
|
||||
required: false,
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
describe("CheckboxControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
shallow(
|
||||
<CheckboxControl {...checkboxProps} managedClasses={managedClasses} />
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate an input HTML element", () => {
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl {...checkboxProps} managedClasses={managedClasses} />
|
||||
);
|
||||
|
||||
expect(rendered.find("input")).toHaveLength(1);
|
||||
});
|
||||
test("should have an `id` attribute on the HTML input element", () => {
|
||||
const dataLocation: string = "foo";
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
dataLocation={dataLocation}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const input: any = rendered.find("input");
|
||||
|
||||
expect(dataLocation).toMatch(input.prop("id"));
|
||||
});
|
||||
test("should fire an `onChange` callback with the input is changed", () => {
|
||||
const handleChange: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
onChange={handleChange}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered
|
||||
.find("input")
|
||||
.at(0)
|
||||
.simulate("change", { target: { checked: true } });
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
expect(handleChange.mock.calls[0][0]).toEqual({ value: true });
|
||||
});
|
||||
test("should be disabled when disabled props is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
disabled={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
const wrapper: any = rendered.find("div");
|
||||
const input: any = rendered.find("input");
|
||||
|
||||
expect(input).toHaveLength(1);
|
||||
expect(input.prop("disabled")).toBeTruthy();
|
||||
expect(wrapper.find(`.${managedClasses.checkboxControl__disabled}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
});
|
||||
test("should have the default class when default prop is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
value={undefined}
|
||||
default={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.checkboxControl__default}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
});
|
||||
test("should show default values if they exist and no data is available", () => {
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
managedClasses={managedClasses}
|
||||
value={undefined}
|
||||
default={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find("input").at(0).prop("value")).toBe(true.toString());
|
||||
});
|
||||
test("should not show default values if data exists", () => {
|
||||
const rendered: any = mount(
|
||||
<CheckboxControl
|
||||
{...checkboxProps}
|
||||
managedClasses={managedClasses}
|
||||
value={false}
|
||||
default={true}
|
||||
/>
|
||||
);
|
||||
expect(rendered.find("input").at(0).prop("value")).toBe(false.toString());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { applyFocusVisible } from "@microsoft/fast-jss-utilities";
|
||||
import {
|
||||
errorColorCSSProperty,
|
||||
insetStrongBoxShadow,
|
||||
L3FillColorProperty,
|
||||
textColorCSSProperty,
|
||||
} from "../../style";
|
||||
|
||||
/**
|
||||
* Checkbox class name contract
|
||||
*/
|
||||
export interface CheckboxControlClassNameContract {
|
||||
checkboxControl?: string;
|
||||
checkboxControl__disabled?: string;
|
||||
checkboxControl_input?: string;
|
||||
checkboxControl_checkmark?: string;
|
||||
checkboxControl__default?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<CheckboxControlClassNameContract, {}> = {
|
||||
checkboxControl: {
|
||||
position: "relative",
|
||||
height: "14px",
|
||||
width: "14px",
|
||||
},
|
||||
checkboxControl_input: {
|
||||
position: "absolute",
|
||||
appearance: "none",
|
||||
minWidth: "14px",
|
||||
height: "14px",
|
||||
boxSizing: "border-box",
|
||||
borderRadius: "2px",
|
||||
border: "1px solid transparent",
|
||||
zIndex: "1",
|
||||
margin: "0",
|
||||
"&:disabled": {
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
"&:hover": {
|
||||
border: `1px solid ${textColorCSSProperty}`,
|
||||
},
|
||||
...applyFocusVisible({
|
||||
outline: "none",
|
||||
...insetStrongBoxShadow(textColorCSSProperty),
|
||||
}),
|
||||
"&:checked": {
|
||||
"& + $checkboxControl_checkmark": {
|
||||
"&::before": {
|
||||
height: "3px",
|
||||
left: "4px",
|
||||
top: "7px",
|
||||
transform: "rotate(-45deg)",
|
||||
},
|
||||
"&::after": {
|
||||
height: "8px",
|
||||
left: "8px",
|
||||
top: "2px",
|
||||
transform: "rotate(45deg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"&:invalid": {
|
||||
borderColor: errorColorCSSProperty,
|
||||
},
|
||||
"&$checkboxControl__default": {
|
||||
"& + span": {
|
||||
"&::after, &::before": {
|
||||
background: textColorCSSProperty,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
checkboxControl_checkmark: {
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
width: "14px",
|
||||
height: "14px",
|
||||
background: L3FillColorProperty,
|
||||
"&::after, &::before": {
|
||||
position: "absolute",
|
||||
display: "block",
|
||||
content: "''",
|
||||
width: "1px",
|
||||
background: textColorCSSProperty,
|
||||
},
|
||||
},
|
||||
checkboxControl__disabled: {},
|
||||
checkboxControl__default: {},
|
||||
};
|
||||
|
||||
export default styles;
|
|
@ -0,0 +1,72 @@
|
|||
import React from "react";
|
||||
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
|
||||
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import styles, { CheckboxControlClassNameContract } from "./control.checkbox.style";
|
||||
import { CheckboxControlProps } from "./control.checkbox.props";
|
||||
import { classNames } from "@microsoft/fast-web-utilities";
|
||||
import { isDefault } from "./utilities/form";
|
||||
|
||||
/**
|
||||
* Form control definition
|
||||
* @extends React.Component
|
||||
*/
|
||||
class CheckboxControl extends React.Component<
|
||||
CheckboxControlProps & ManagedClasses<CheckboxControlClassNameContract>,
|
||||
{}
|
||||
> {
|
||||
public static displayName: string = "CheckboxControl";
|
||||
|
||||
public static defaultProps: Partial<
|
||||
CheckboxControlProps & ManagedClasses<CheckboxControlClassNameContract>
|
||||
> = {
|
||||
managedClasses: {},
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const value: boolean =
|
||||
typeof this.props.value === "boolean"
|
||||
? this.props.value
|
||||
: typeof this.props.default === "boolean"
|
||||
? this.props.default
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
this.props.managedClasses.checkboxControl,
|
||||
[
|
||||
this.props.managedClasses.checkboxControl__disabled,
|
||||
this.props.disabled,
|
||||
],
|
||||
[
|
||||
this.props.managedClasses.checkboxControl__default,
|
||||
isDefault(this.props.value, this.props.default),
|
||||
]
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className={this.props.managedClasses.checkboxControl_input}
|
||||
id={this.props.dataLocation}
|
||||
type={"checkbox"}
|
||||
value={value.toString()}
|
||||
onChange={this.handleChange()}
|
||||
checked={value}
|
||||
disabled={this.props.disabled}
|
||||
ref={this.props.elementRef as React.Ref<HTMLInputElement>}
|
||||
onFocus={this.props.reportValidity}
|
||||
onBlur={this.props.updateValidity}
|
||||
/>
|
||||
<span className={this.props.managedClasses.checkboxControl_checkmark} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange = (): ((e: React.ChangeEvent<HTMLInputElement>) => void) => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.props.onChange({ value: e.target.checked });
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export { CheckboxControl };
|
||||
export default manageJss(styles)(CheckboxControl);
|
|
@ -0,0 +1,3 @@
|
|||
import { CommonControlConfig } from "../templates";
|
||||
|
||||
export type DisplayControlProps = CommonControlConfig;
|
|
@ -0,0 +1,142 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount, shallow } from "enzyme";
|
||||
import { DisplayControl } from "./control.display";
|
||||
import { DisplayControlProps } from "./control.display.props";
|
||||
import { DisplayControlClassNameContract } from "./control.display.style";
|
||||
import { ControlType } from "../templates";
|
||||
import defaultStrings from "../form.strings";
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const managedClasses: DisplayControlClassNameContract = {
|
||||
displayControl: "displayControl",
|
||||
displayControl__disabled: "displayControl__disabled",
|
||||
displayControl__default: "displayControl__default",
|
||||
};
|
||||
|
||||
const displayProps: DisplayControlProps = {
|
||||
type: ControlType.display,
|
||||
dataLocation: "",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
navigation: {},
|
||||
value: "",
|
||||
schema: {},
|
||||
disabled: false,
|
||||
reportValidity: jest.fn(),
|
||||
updateValidity: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
elementRef: null,
|
||||
validationErrors: [],
|
||||
required: false,
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
describe("DisplayControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
shallow(<DisplayControl {...displayProps} managedClasses={managedClasses} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate an HTML input element", () => {
|
||||
const rendered: any = mount(
|
||||
<DisplayControl {...displayProps} managedClasses={managedClasses} />
|
||||
);
|
||||
|
||||
expect(rendered.find("input")).toHaveLength(1);
|
||||
});
|
||||
test("should be disabled when disabled props is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
disabled={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.displayControl__disabled}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
expect(rendered.find("input").prop("disabled")).toBeTruthy();
|
||||
});
|
||||
test("should have the default class when default prop is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
value={undefined}
|
||||
default={"foo"}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.displayControl__default}`)).toHaveLength(
|
||||
1
|
||||
);
|
||||
});
|
||||
test("should show default values if they exist and no data is available", () => {
|
||||
const defaultValue: string = "bar";
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
managedClasses={managedClasses}
|
||||
value={undefined}
|
||||
default={defaultValue}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
|
||||
defaultValue
|
||||
);
|
||||
});
|
||||
test("should show non-string default values if they exist and no data is available", () => {
|
||||
const defaultValue: number = 50;
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
managedClasses={managedClasses}
|
||||
value={undefined}
|
||||
default={defaultValue}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
|
||||
JSON.stringify(defaultValue, null, 2)
|
||||
);
|
||||
});
|
||||
test("should not show default values if data exists", () => {
|
||||
const value: string = "foo";
|
||||
const defaultValue: string = "bar";
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
managedClasses={managedClasses}
|
||||
value={value}
|
||||
default={defaultValue}
|
||||
/>
|
||||
);
|
||||
expect(rendered.find(`.${managedClasses.displayControl}`).prop("value")).toBe(
|
||||
value
|
||||
);
|
||||
});
|
||||
test("should NOT fire an onChange when the input value is updated", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<DisplayControl
|
||||
{...displayProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered.find("input").simulate("change", { target: { value: "foo" } });
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { defaultFontStyle, inputStyle } from "../../style";
|
||||
|
||||
/**
|
||||
* Display class name contract
|
||||
*/
|
||||
export interface DisplayControlClassNameContract {
|
||||
displayControl?: string;
|
||||
displayControl__disabled?: string;
|
||||
displayControl__default?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<DisplayControlClassNameContract, {}> = {
|
||||
displayControl: {
|
||||
...inputStyle,
|
||||
width: "100%",
|
||||
"&$displayControl__default": {
|
||||
...defaultFontStyle,
|
||||
},
|
||||
},
|
||||
displayControl__disabled: {},
|
||||
displayControl__default: {},
|
||||
};
|
||||
|
||||
export default styles;
|
|
@ -0,0 +1,81 @@
|
|||
import React from "react";
|
||||
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
|
||||
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import styles, { DisplayControlClassNameContract } from "./control.display.style";
|
||||
import { DisplayControlProps } from "./control.display.props";
|
||||
import { classNames } from "@microsoft/fast-web-utilities";
|
||||
import { isDefault } from "./utilities/form";
|
||||
|
||||
/**
|
||||
* Form control definition
|
||||
*/
|
||||
class DisplayControl extends React.Component<
|
||||
DisplayControlProps & ManagedClasses<DisplayControlClassNameContract>,
|
||||
{}
|
||||
> {
|
||||
public static displayName: string = "DisplayControl";
|
||||
|
||||
public static defaultProps: Partial<
|
||||
DisplayControlProps & ManagedClasses<DisplayControlClassNameContract>
|
||||
> = {
|
||||
managedClasses: {},
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<input
|
||||
className={classNames(
|
||||
this.props.managedClasses.displayControl,
|
||||
[
|
||||
this.props.managedClasses.displayControl__disabled,
|
||||
this.props.disabled,
|
||||
],
|
||||
[
|
||||
this.props.managedClasses.displayControl__default,
|
||||
isDefault(this.props.value, this.props.default),
|
||||
]
|
||||
)}
|
||||
type={"text"}
|
||||
ref={this.props.elementRef as React.Ref<HTMLInputElement>}
|
||||
onBlur={this.props.updateValidity}
|
||||
onFocus={this.props.reportValidity}
|
||||
value={this.getDisplayValue(this.props.value)}
|
||||
onChange={this.handleInputChange}
|
||||
disabled={this.props.disabled}
|
||||
required={this.props.required}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dummy onChange handler
|
||||
*
|
||||
* Form elements will not validate against read-only form items
|
||||
* therefore a value and onChange handler must still be supplied
|
||||
* even if there is no intention to update the value.
|
||||
*/
|
||||
private handleInputChange = (): void => {};
|
||||
|
||||
private getDisplayValue(data: any): string {
|
||||
const typeofData: string = typeof data;
|
||||
const typeofDefault: string = typeof this.props.default;
|
||||
|
||||
if (typeofData === "undefined" && typeofDefault !== "undefined") {
|
||||
if (typeofDefault === "string") {
|
||||
return this.props.default;
|
||||
}
|
||||
|
||||
return JSON.stringify(this.props.default, null, 2);
|
||||
}
|
||||
|
||||
switch (typeofData) {
|
||||
case "string":
|
||||
return data;
|
||||
default:
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { DisplayControl };
|
||||
export default manageJss(styles)(DisplayControl);
|
|
@ -0,0 +1,23 @@
|
|||
import { DragState, LinkedDataControlConfig } from "../templates";
|
||||
|
||||
export interface ChildComponentDataMapping {
|
||||
[T: string]: any;
|
||||
}
|
||||
|
||||
export type LinkedDataControlProps = LinkedDataControlConfig;
|
||||
|
||||
export enum Action {
|
||||
add = "add",
|
||||
edit = "edit",
|
||||
delete = "delete",
|
||||
}
|
||||
|
||||
/**
|
||||
* State object for the LinkedDataControl component
|
||||
*/
|
||||
export interface LinkedDataControlState extends DragState {
|
||||
/**
|
||||
* The search term used to filter a list of linked data
|
||||
*/
|
||||
searchTerm: string;
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount, ReactWrapper } from "enzyme";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import {
|
||||
keyArrowDown,
|
||||
keyArrowUp,
|
||||
keyEnter,
|
||||
keyTab,
|
||||
} from "@microsoft/fast-web-utilities";
|
||||
import { ControlType } from "../templates";
|
||||
import { LinkedDataActionType } from "../templates/types";
|
||||
import defaultStrings from "../form.strings";
|
||||
import { LinkedDataControl } from "./control.linked-data";
|
||||
import { LinkedDataControlProps } from "./control.linked-data.props";
|
||||
import { LinkedDataControlClassNameContract } from "./control.linked-data.style";
|
||||
|
||||
const LinkedDataFormControlWithDragAndDrop: React.FC<any> = (
|
||||
props: React.PropsWithChildren<any>
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<LinkedDataControl {...props} />
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const managedClasses: LinkedDataControlClassNameContract = {
|
||||
linkedDataControl: "linkedDataFormControl-class",
|
||||
linkedDataControl_linkedDataListControl:
|
||||
"linkedDataFormControl_linkedDataListControl-class",
|
||||
linkedDataControl_linkedDataListInput:
|
||||
"linkedDataFormControl_linkedDataListInput-class",
|
||||
linkedDataControl_delete: "linkedDataFormControl_delete-class",
|
||||
linkedDataControl_deleteButton: "linkedDataFormControl_deleteButton-class",
|
||||
linkedDataControl_existingLinkedData:
|
||||
"linkedDataFormControl_existingLinkedData-class",
|
||||
linkedDataControl_existingLinkedDataItem:
|
||||
"linkedDataFormControl_existingLinkedDataItem-class",
|
||||
linkedDataControl_existingLinkedDataItemContent:
|
||||
"linkedDataFormControl_existingLinkedDataItemContent-class",
|
||||
linkedDataControl_existingLinkedDataItemLink:
|
||||
"linkedDataFormControl_existingLinkedDataItemLink-class",
|
||||
linkedDataControl_existingLinkedDataItemName:
|
||||
"linkedDataFormControl_existingLinkedDataItemName-class",
|
||||
};
|
||||
|
||||
const linkedDataProps: LinkedDataControlProps = {
|
||||
type: ControlType.linkedData,
|
||||
schemaDictionary: {
|
||||
alpha: {
|
||||
title: "alpha",
|
||||
alias: "A",
|
||||
id: "alpha",
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
beta: {
|
||||
title: "beta",
|
||||
id: "beta",
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
omega: {
|
||||
title: "omega",
|
||||
id: "omega",
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
dataLocation: "locationOfLinkedData",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
dataDictionary: [
|
||||
{
|
||||
"": {
|
||||
schemaId: "alpha",
|
||||
data: {},
|
||||
},
|
||||
foo: {
|
||||
schemaId: "beta",
|
||||
data: {},
|
||||
},
|
||||
},
|
||||
"",
|
||||
],
|
||||
navigation: {},
|
||||
value: undefined,
|
||||
schema: {},
|
||||
onChange: jest.fn(),
|
||||
onUpdateSection: jest.fn(),
|
||||
reportValidity: jest.fn(),
|
||||
updateValidity: jest.fn(),
|
||||
disabled: false,
|
||||
elementRef: null,
|
||||
validationErrors: [],
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
describe("LinkedDataControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate a text input", () => {
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find("input")).toHaveLength(1);
|
||||
});
|
||||
test("should add a datalist", () => {
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find("datalist")).toHaveLength(1);
|
||||
});
|
||||
test("should add an `aria-controls` on a text input with the same value as the id of the `listbox`", () => {
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const inputAriaControls: string = rendered.find("input").props()["aria-controls"];
|
||||
const datalist: string = rendered.find("datalist").props()["id"];
|
||||
|
||||
expect(inputAriaControls).toEqual(datalist);
|
||||
});
|
||||
test("should add a `label` to an option if it's schema contains an `alias` property", () => {
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const listboxItems: any = rendered.find("datalist option");
|
||||
|
||||
expect(listboxItems.at(0).prop("label")).toEqual("A");
|
||||
});
|
||||
test("should generate options based on the schema items in the schema dictionary", () => {
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const listboxItems: any = rendered.find("datalist option");
|
||||
|
||||
expect(listboxItems).toHaveLength(3);
|
||||
});
|
||||
test("should show if linkedData are present in the data as a DragItem", () => {
|
||||
const renderedWithOneChild: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
value={[{ id: "foo" }]}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderedWithOneChild.find("DragItem")).toHaveLength(1);
|
||||
});
|
||||
test("should not update the value to match when there are multiple matching values", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const targetValue: any = { value: "a" };
|
||||
const input: any = rendered.find("input");
|
||||
input.simulate("change", { target: targetValue });
|
||||
input.simulate("keydown", { key: keyTab });
|
||||
|
||||
expect(input.getDOMNode().value).toEqual("a");
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
test("should update the value to match when there is a single matching value", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const targetValue: any = { value: "ome" };
|
||||
const input: any = rendered.find("input");
|
||||
input.simulate("change", { target: targetValue });
|
||||
input.simulate("keydown", { key: keyTab });
|
||||
|
||||
expect(input.getDOMNode().value).toEqual("omega");
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
test("should fire a callback to update the data when the value is an exact match and enter has been pressed", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const targetValue: any = { value: "omega" };
|
||||
const input: any = rendered.find("input");
|
||||
input.simulate("change", { target: targetValue });
|
||||
input.simulate("keydown", { key: keyEnter });
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
expect(callback.mock.calls[0][0]).toEqual({
|
||||
index: 0,
|
||||
isLinkedData: true,
|
||||
linkedDataAction: LinkedDataActionType.add,
|
||||
value: [
|
||||
{
|
||||
data: {},
|
||||
parent: {
|
||||
dataLocation: "locationOfLinkedData",
|
||||
id: "",
|
||||
},
|
||||
schemaId: "omega",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
test("should update active section to the linked data item clicked", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: ReactWrapper = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
value={[{ id: "foo" }]}
|
||||
onUpdateSection={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered
|
||||
.find(`li.${managedClasses.linkedDataControl_existingLinkedDataItem} > a`)
|
||||
.simulate("click");
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
expect(callback.mock.calls[0][0]).toEqual("foo");
|
||||
});
|
||||
test("should not fire a callback to update the data when the value is changed but there is no match", () => {
|
||||
const callback: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
onChange={callback}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
const targetValue: any = { value: "no match" };
|
||||
rendered
|
||||
.find("input")
|
||||
.simulate("change", { target: targetValue, currentTarget: targetValue });
|
||||
rendered.find("input").simulate("keydown", { key: keyEnter });
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
test("should remove a child option from the data when the remove button has been clicked", () => {
|
||||
const callback: any = jest.fn();
|
||||
const renderedWithTwoLinkedData: any = mount(
|
||||
<LinkedDataFormControlWithDragAndDrop
|
||||
{...linkedDataProps}
|
||||
managedClasses={managedClasses}
|
||||
value={[
|
||||
{
|
||||
id: "foo",
|
||||
},
|
||||
]}
|
||||
onChange={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
renderedWithTwoLinkedData.find("DragItem").find("button").simulate("click");
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
expect(callback.mock.calls[0][0]).toEqual({
|
||||
isLinkedData: true,
|
||||
linkedDataAction: LinkedDataActionType.remove,
|
||||
value: [
|
||||
{
|
||||
id: "foo",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { ellipsis } from "@microsoft/fast-jss-utilities";
|
||||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import {
|
||||
cleanListStyle,
|
||||
removeItemStyle,
|
||||
selectInputStyle,
|
||||
selectSpanStyle,
|
||||
softRemoveStyle,
|
||||
} from "../../style";
|
||||
|
||||
/**
|
||||
* LinkedData class name contract
|
||||
*/
|
||||
export interface LinkedDataControlClassNameContract {
|
||||
linkedDataControl?: string;
|
||||
linkedDataControl_existingLinkedData?: string;
|
||||
linkedDataControl_existingLinkedDataItem?: string;
|
||||
linkedDataControl_existingLinkedDataItemLink?: string;
|
||||
linkedDataControl_existingLinkedDataItemContent?: string;
|
||||
linkedDataControl_existingLinkedDataItemName?: string;
|
||||
linkedDataControl_linkedDataListControl?: string;
|
||||
linkedDataControl_linkedDataListInput?: string;
|
||||
linkedDataControl_linkedDataListInputRegion?: string;
|
||||
linkedDataControl_delete?: string;
|
||||
linkedDataControl_deleteButton?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<LinkedDataControlClassNameContract, {}> = {
|
||||
linkedDataControl: {},
|
||||
linkedDataControl_existingLinkedData: {
|
||||
...cleanListStyle,
|
||||
},
|
||||
linkedDataControl_existingLinkedDataItem: {
|
||||
position: "relative",
|
||||
height: "30px",
|
||||
marginLeft: "-10px",
|
||||
paddingLeft: "10px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.04)",
|
||||
},
|
||||
},
|
||||
linkedDataControl_existingLinkedDataItemLink: {
|
||||
width: "calc(100% - 36px)",
|
||||
"&$linkedDataControl_existingLinkedDataItemName, &$linkedDataControl_existingLinkedDataItemContent": {
|
||||
...ellipsis(),
|
||||
width: "100%",
|
||||
display: "inline-block",
|
||||
verticalAlign: "bottom",
|
||||
},
|
||||
},
|
||||
linkedDataControl_existingLinkedDataItemName: {
|
||||
...ellipsis(),
|
||||
display: "inline-block",
|
||||
width: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
linkedDataControl_existingLinkedDataItemContent: {},
|
||||
linkedDataControl_linkedDataListControl: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
},
|
||||
linkedDataControl_linkedDataListInput: {
|
||||
...selectInputStyle,
|
||||
"&::-webkit-calendar-picker-indicator": {
|
||||
display: "none !important",
|
||||
},
|
||||
},
|
||||
linkedDataControl_linkedDataListInputRegion: {
|
||||
...selectSpanStyle,
|
||||
},
|
||||
linkedDataControl_delete: {
|
||||
...softRemoveStyle,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
linkedDataControl_deleteButton: {
|
||||
...removeItemStyle,
|
||||
cursor: "pointer",
|
||||
},
|
||||
};
|
||||
|
||||
export default styles;
|
|
@ -0,0 +1,338 @@
|
|||
import { ManagedClasses } from "@microsoft/fast-components-class-name-contracts-base";
|
||||
import React from "react";
|
||||
import { keyEnter, keyTab } from "@microsoft/fast-web-utilities";
|
||||
import { getDataFromSchema } from "@microsoft/fast-tooling";
|
||||
import manageJss, { ManagedJSSProps } from "@microsoft/fast-jss-manager-react";
|
||||
import { DragItem } from "../templates";
|
||||
import { ArrayAction, LinkedDataActionType } from "../templates/types";
|
||||
import styles, { LinkedDataControlClassNameContract } from "./control.linked-data.style";
|
||||
import {
|
||||
LinkedDataControlProps,
|
||||
LinkedDataControlState,
|
||||
} from "./control.linked-data.props";
|
||||
|
||||
/**
|
||||
* Form control definition
|
||||
*/
|
||||
class LinkedDataControl extends React.Component<
|
||||
LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>,
|
||||
LinkedDataControlState
|
||||
> {
|
||||
public static displayName: string = "LinkedDataControl";
|
||||
|
||||
public static defaultProps: Partial<
|
||||
LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>
|
||||
> = {
|
||||
managedClasses: {},
|
||||
};
|
||||
|
||||
constructor(
|
||||
props: LinkedDataControlProps & ManagedClasses<LinkedDataControlClassNameContract>
|
||||
) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchTerm: "",
|
||||
isDragging: false,
|
||||
data: [].concat(props.value || []),
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
// Convert to search component when #3006 has been completed
|
||||
return (
|
||||
<div className={this.props.managedClasses.linkedDataControl}>
|
||||
{this.renderExistingLinkedData()}
|
||||
{this.renderAddLinkedData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the UI for adding linked data
|
||||
*/
|
||||
private renderAddLinkedData(): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
this.props.managedClasses.linkedDataControl_linkedDataListControl
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
this.props.managedClasses
|
||||
.linkedDataControl_linkedDataListInputRegion
|
||||
}
|
||||
>
|
||||
<input
|
||||
className={
|
||||
this.props.managedClasses
|
||||
.linkedDataControl_linkedDataListInput
|
||||
}
|
||||
type={"text"}
|
||||
aria-autocomplete={"list"}
|
||||
list={this.getLinkedDataInputId()}
|
||||
aria-controls={this.getLinkedDataInputId()}
|
||||
value={this.state.searchTerm}
|
||||
placeholder={this.props.strings.linkedDataPlaceholder}
|
||||
onChange={this.handleSearchTermUpdate}
|
||||
onKeyDown={this.handleLinkedDataKeydown}
|
||||
/>
|
||||
</span>
|
||||
<datalist id={this.getLinkedDataInputId()} role={"listbox"}>
|
||||
{this.renderFilteredLinkedDataOptions()}
|
||||
</datalist>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderFilteredLinkedDataOptions(): React.ReactNode {
|
||||
return Object.entries(this.props.schemaDictionary).map(
|
||||
([key, value]: [string, any]): React.ReactNode => {
|
||||
return (
|
||||
<option
|
||||
key={key}
|
||||
value={value.title}
|
||||
label={typeof value.alias === "string" ? value.alias : void 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of existing linkedData for a component
|
||||
*/
|
||||
private renderExistingLinkedData(): React.ReactNode {
|
||||
const childItems: React.ReactNode = this.renderExistingLinkedDataItem();
|
||||
|
||||
if (childItems) {
|
||||
return (
|
||||
<ul
|
||||
className={
|
||||
this.props.managedClasses.linkedDataControl_existingLinkedData
|
||||
}
|
||||
>
|
||||
{childItems}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private renderExistingLinkedDataItem(): React.ReactNode {
|
||||
if (Array.isArray(this.props.value)) {
|
||||
const {
|
||||
linkedDataControl_existingLinkedDataItem,
|
||||
linkedDataControl_existingLinkedDataItemLink,
|
||||
linkedDataControl_deleteButton,
|
||||
}: LinkedDataControlClassNameContract = this.props.managedClasses;
|
||||
|
||||
return (this.state.isDragging ? this.state.data : this.props.value).map(
|
||||
(value: any, index: number) => {
|
||||
return (
|
||||
<DragItem
|
||||
key={value + index}
|
||||
itemClassName={linkedDataControl_existingLinkedDataItem}
|
||||
itemLinkClassName={
|
||||
linkedDataControl_existingLinkedDataItemLink
|
||||
}
|
||||
itemRemoveClassName={linkedDataControl_deleteButton}
|
||||
minItems={0}
|
||||
itemLength={1}
|
||||
index={index}
|
||||
onClick={this.handleItemClick(value.id)}
|
||||
removeDragItem={this.handleRemoveItem}
|
||||
moveDragItem={this.handleMoveItem}
|
||||
dropDragItem={this.handleDropItem}
|
||||
dragStart={this.handleDragStart}
|
||||
dragEnd={this.handleDragEnd}
|
||||
strings={this.props.strings}
|
||||
>
|
||||
{
|
||||
this.props.schemaDictionary[
|
||||
this.props.dataDictionary[0][value.id].schemaId
|
||||
].title
|
||||
}
|
||||
</DragItem>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleDragStart = (): void => {
|
||||
this.setState({
|
||||
isDragging: true,
|
||||
data: [].concat(this.props.value || []),
|
||||
});
|
||||
};
|
||||
|
||||
private handleDragEnd = (): void => {
|
||||
this.setState({
|
||||
isDragging: false,
|
||||
});
|
||||
};
|
||||
|
||||
private handleItemClick = (
|
||||
id: string
|
||||
): ((index: number) => (e: React.MouseEvent<HTMLAnchorElement>) => void) => {
|
||||
return (index: number): ((e: React.MouseEvent<HTMLAnchorElement>) => void) => {
|
||||
return (e: React.MouseEvent<HTMLAnchorElement>): void => {
|
||||
this.props.onUpdateSection(id);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
private handleRemoveItem = (
|
||||
type: ArrayAction,
|
||||
index: number
|
||||
): ((e: React.MouseEvent<HTMLButtonElement>) => void) => {
|
||||
return (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
this.props.onChange({
|
||||
value: this.props.value.splice(index, 1),
|
||||
isLinkedData: true,
|
||||
linkedDataAction: LinkedDataActionType.remove,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
private handleMoveItem = (sourceIndex: number, targetIndex: number): void => {
|
||||
const currentData: unknown[] = [].concat(this.props.value);
|
||||
|
||||
if (sourceIndex !== targetIndex) {
|
||||
currentData.splice(targetIndex, 0, currentData.splice(sourceIndex, 1)[0]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
data: currentData,
|
||||
});
|
||||
};
|
||||
|
||||
private handleDropItem = (): void => {
|
||||
this.props.onChange({
|
||||
value: this.state.data,
|
||||
isLinkedData: true,
|
||||
linkedDataAction: LinkedDataActionType.reorder,
|
||||
});
|
||||
};
|
||||
|
||||
private handleLinkedDataKeydown = (
|
||||
e: React.KeyboardEvent<HTMLInputElement>
|
||||
): void => {
|
||||
if (e.target === e.currentTarget) {
|
||||
// Enter adds linked data if the input value matches a schema lazily or exactly
|
||||
if (e.key === keyEnter) {
|
||||
e.preventDefault();
|
||||
|
||||
const normalizedValue = e.currentTarget.value.toLowerCase();
|
||||
|
||||
if (
|
||||
this.lazyMatchValueWithASingleSchema(normalizedValue) ||
|
||||
this.matchExactValueWithASingleSchema(e.currentTarget.value)
|
||||
) {
|
||||
this.addLinkedData(normalizedValue, e.currentTarget.value);
|
||||
|
||||
/**
|
||||
* Adding items to the linked data causes the items to
|
||||
* move the input down while the datalist remains in the same location,
|
||||
* to prevent the datalist from overlapping the input
|
||||
* the datalist is dismissed by defocusing and refocusing the input
|
||||
*/
|
||||
(e.target as HTMLElement).blur();
|
||||
(e.target as HTMLElement).focus();
|
||||
|
||||
this.setState({
|
||||
searchTerm: "",
|
||||
});
|
||||
}
|
||||
// Tab performs an auto-complete if there is a single schema it can match to
|
||||
} else if (e.key === keyTab) {
|
||||
const normalizedValue = e.currentTarget.value.toLowerCase();
|
||||
const matchedSchema = this.lazyMatchValueWithASingleSchema(
|
||||
normalizedValue
|
||||
);
|
||||
|
||||
if (typeof matchedSchema === "string") {
|
||||
// prevent navigating away by tab when single schema matched
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
searchTerm: this.props.schemaDictionary[matchedSchema].title,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private lazyMatchValueWithASingleSchema(value: string): string | void {
|
||||
const matchingSchemas: string[] = Object.keys(this.props.schemaDictionary).reduce<
|
||||
string[]
|
||||
>((previousValue: string[], currentValue: string): string[] => {
|
||||
if (
|
||||
this.props.schemaDictionary[currentValue].title
|
||||
.toLowerCase()
|
||||
.includes(value)
|
||||
) {
|
||||
return previousValue.concat([currentValue]);
|
||||
}
|
||||
|
||||
return previousValue;
|
||||
}, []);
|
||||
|
||||
if (matchingSchemas.length === 1) {
|
||||
return matchingSchemas[0];
|
||||
}
|
||||
}
|
||||
|
||||
private matchExactValueWithASingleSchema(value: string): string | void {
|
||||
return Object.keys(this.props.schemaDictionary).find(
|
||||
(schemaDictionaryKey: string) => {
|
||||
return value === this.props.schemaDictionary[schemaDictionaryKey].title;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change handler for editing the search term filter
|
||||
*/
|
||||
private handleSearchTermUpdate = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
private addLinkedData(normalizedValue: string, originalValue: string): void {
|
||||
const matchedNormalizedValue:
|
||||
| string
|
||||
| void = this.lazyMatchValueWithASingleSchema(normalizedValue);
|
||||
const matchedOriginalValue: string | void = this.matchExactValueWithASingleSchema(
|
||||
originalValue
|
||||
);
|
||||
const schemaId: string | void = matchedNormalizedValue || matchedOriginalValue;
|
||||
|
||||
if (typeof schemaId !== "undefined") {
|
||||
this.props.onChange({
|
||||
value: [
|
||||
{
|
||||
schemaId,
|
||||
parent: {
|
||||
id: this.props.dictionaryId,
|
||||
dataLocation: this.props.dataLocation,
|
||||
},
|
||||
data: getDataFromSchema(this.props.schemaDictionary[schemaId]),
|
||||
},
|
||||
],
|
||||
isLinkedData: true,
|
||||
linkedDataAction: LinkedDataActionType.add,
|
||||
index: Array.isArray(this.props.value) ? this.props.value.length : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getLinkedDataInputId(): string {
|
||||
return `${this.props.dataLocation}-input`;
|
||||
}
|
||||
}
|
||||
|
||||
export { LinkedDataControl };
|
||||
export default manageJss(styles)(LinkedDataControl);
|
|
@ -0,0 +1,3 @@
|
|||
import { NumberFieldTypeControlConfig } from "../templates";
|
||||
|
||||
export type NumberFieldControlProps = NumberFieldTypeControlConfig;
|
|
@ -0,0 +1,170 @@
|
|||
import React from "react";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
import "../../__tests__/mocks/match-media";
|
||||
import { configure, mount, shallow } from "enzyme";
|
||||
import { NumberFieldControl } from "./control.number-field";
|
||||
import { NumberFieldControlProps } from "./control.number-field.props";
|
||||
import { NumberFieldControlClassNameContract } from "./control.number-field.style";
|
||||
import { ControlType } from "../templates";
|
||||
import defaultStrings from "../form.strings";
|
||||
|
||||
/*
|
||||
* Configure Enzyme
|
||||
*/
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const managedClasses: NumberFieldControlClassNameContract = {
|
||||
numberFieldControl: "numberFieldControl-class",
|
||||
numberFieldControl__disabled: "numberFieldControl__disabled-class",
|
||||
numberFieldControl__default: "numberFieldControl__default-class",
|
||||
};
|
||||
|
||||
const numberFieldProps: NumberFieldControlProps = {
|
||||
type: ControlType.numberField,
|
||||
dataLocation: "",
|
||||
navigationConfigId: "",
|
||||
dictionaryId: "",
|
||||
navigation: {},
|
||||
onChange: jest.fn(),
|
||||
min: 0,
|
||||
max: Infinity,
|
||||
step: 1,
|
||||
value: 0,
|
||||
schema: {},
|
||||
disabled: false,
|
||||
elementRef: null,
|
||||
reportValidity: jest.fn(),
|
||||
updateValidity: jest.fn(),
|
||||
validationErrors: [],
|
||||
required: false,
|
||||
messageSystem: void 0,
|
||||
strings: defaultStrings,
|
||||
messageSystemOptions: null,
|
||||
};
|
||||
|
||||
describe("NumberFieldControl", () => {
|
||||
test("should not throw", () => {
|
||||
expect(() => {
|
||||
shallow(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
test("should generate an HTML input element", () => {
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl {...numberFieldProps} managedClasses={managedClasses} />
|
||||
);
|
||||
|
||||
expect(rendered.find("input")).toHaveLength(1);
|
||||
});
|
||||
test("should be disabled when disabled props is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
disabled={true}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
expect(rendered.find("input")).toHaveLength(1);
|
||||
expect(rendered.find("input").prop("disabled")).toBeTruthy();
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.numberFieldControl__disabled}`)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
test("should have the default class when default prop is passed", () => {
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
value={undefined}
|
||||
default={42}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.find(`.${managedClasses.numberFieldControl__default}`)
|
||||
).toHaveLength(1);
|
||||
});
|
||||
test("should fire an `onChange` callback with the input is changed", () => {
|
||||
const handleChange: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
onChange={handleChange}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered
|
||||
.find("input")
|
||||
.at(0)
|
||||
.simulate("change", { target: { value: "1" } }); // The target.value from an input text box is always a string.
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
expect(handleChange.mock.calls[0][0]).toEqual({ value: 1 });
|
||||
});
|
||||
test("should fire an `onChange` callback with undefined value if the input is an empty string", () => {
|
||||
const handleChange: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
onChange={handleChange}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered
|
||||
.find("input")
|
||||
.at(0)
|
||||
.simulate("change", { target: { value: "" } });
|
||||
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
expect(handleChange.mock.calls[0][0]).toEqual({ value: undefined });
|
||||
});
|
||||
test("should not fire an `onChange` callback if the input is NaN", () => {
|
||||
const handleChange: any = jest.fn();
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
onChange={handleChange}
|
||||
managedClasses={managedClasses}
|
||||
/>
|
||||
);
|
||||
|
||||
rendered
|
||||
.find("input")
|
||||
.at(0)
|
||||
.simulate("change", { target: { value: "foo" } });
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
test("should show default values if they exist and no data is available", () => {
|
||||
const defaultValue: number = 5;
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
managedClasses={managedClasses}
|
||||
value={undefined}
|
||||
default={defaultValue}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(rendered.find("input").prop("value")).toBe(defaultValue);
|
||||
});
|
||||
test("should not show default values if data exists", () => {
|
||||
const value: number = 5;
|
||||
const defaultValue: number = 10;
|
||||
const rendered: any = mount(
|
||||
<NumberFieldControl
|
||||
{...numberFieldProps}
|
||||
managedClasses={managedClasses}
|
||||
value={value}
|
||||
default={defaultValue}
|
||||
/>
|
||||
);
|
||||
expect(rendered.find("input").at(0).prop("value")).toBe(value);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { ComponentStyles } from "@microsoft/fast-jss-manager-react";
|
||||
import { defaultFontStyle, inputStyle } from "../../style";
|
||||
|
||||
/**
|
||||
* Number field class name contract
|
||||
*/
|
||||
export interface NumberFieldControlClassNameContract {
|
||||
numberFieldControl?: string;
|
||||
numberFieldControl__disabled?: string;
|
||||
numberFieldControl__default?: string;
|
||||
}
|
||||
|
||||
const styles: ComponentStyles<NumberFieldControlClassNameContract, {}> = {
|
||||
numberFieldControl: {
|
||||
...inputStyle,
|
||||
width: "100%",
|
||||
"&$numberFieldControl__default": {
|
||||
...defaultFontStyle,
|
||||
},
|
||||
},
|
||||
numberFieldControl__disabled: {},
|
||||
numberFieldControl__default: {},
|
||||
};
|
||||
|
||||
export default styles;
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче