From: Michal Klobukowski Date: Thu, 29 Nov 2018 09:43:01 +0000 (+0100) Subject: Merge branch '14393-vocabulary' X-Git-Tag: 1.3.0~5^2^2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/fdc9e2f707b15d520ec0a39e7cfe52ef23a958f4?hp=9dc1cd9abe5d0a9dbd71ae7a95996174e9bf07c4 Merge branch '14393-vocabulary' refs #14393 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/README.md b/README.md index 998d4246..ea9bc02f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ The app will fetch runtime configuration when starting. By default it will try t Currently this configuration schema is supported: ``` { - "API_HOST": "string" + "API_HOST": "string", + "VOCABULARY_URL": "string" } ``` diff --git a/package.json b/package.json index 89458046..64a24dca 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "scripts": { "start": "react-scripts-ts start", "build": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts build", + "build-local": "react-scripts-ts build", "test": "CI=true react-scripts-ts test --env=jsdom", + "test-local": "react-scripts-ts test --env=jsdom", "eject": "react-scripts-ts eject", "lint": "tslint src/** -t verbose", "build-css": "node-sass-chokidar src/ -o src/", diff --git a/src/common/config.ts b/src/common/config.ts index 1ab73294..c74277e4 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -49,6 +49,7 @@ export interface Config { version: string; websocketUrl: string; workbenchUrl: string; + vocabularyUrl: string; } export const fetchConfig = () => { @@ -58,7 +59,10 @@ export const fetchConfig = () => { .catch(() => Promise.resolve(getDefaultConfig())) .then(config => Axios .get(getDiscoveryURL(config.API_HOST)) - .then(response => ({ config: response.data, apiHost: config.API_HOST }))); + .then(response => ({ + config: {...response.data, vocabularyUrl: config.VOCABULARY_URL }, + apiHost: config.API_HOST, + }))); }; @@ -105,15 +109,18 @@ export const mockConfig = (config: Partial): Config => ({ version: '', websocketUrl: '', workbenchUrl: '', + vocabularyUrl: '', ...config }); interface ConfigJSON { API_HOST: string; + VOCABULARY_URL: string; } const getDefaultConfig = (): ConfigJSON => ({ API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "", + VOCABULARY_URL: "", }); const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/discovery/v1/apis/arvados/v1/rest`; diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index 85704c35..7da4ba4a 100644 --- a/src/components/autocomplete/autocomplete.tsx +++ b/src/components/autocomplete/autocomplete.tsx @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List } from '@material-ui/core'; +import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, ListItemText, ListItem, List, FormHelperText } from '@material-ui/core'; import { PopperProps } from '@material-ui/core/Popper'; import { WithStyles } from '@material-ui/core/styles'; import { noop } from 'lodash'; @@ -13,6 +13,8 @@ export interface AutocompleteProps { value: string; items: Item[]; suggestions?: Suggestion[]; + error?: boolean; + helperText?: string; onChange: (event: React.ChangeEvent) => void; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -38,9 +40,10 @@ export class Autocomplete extends React.Component - + {this.renderLabel()} {this.renderInput()} + {this.renderHelperText()} {this.renderSuggestions()} @@ -64,12 +67,16 @@ export class Autocomplete extends React.Component; } + renderHelperText(){ + return {this.props.helperText}; + } + renderSuggestions() { const { suggestions = [] } = this.props; return ( 0} - anchorEl={this.containerRef.current}> + anchorEl={this.inputRef.current}> {suggestions.map( diff --git a/src/components/progress-button/progress-button.tsx b/src/components/progress-button/progress-button.tsx new file mode 100644 index 00000000..14286dd2 --- /dev/null +++ b/src/components/progress-button/progress-button.tsx @@ -0,0 +1,36 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import Button, { ButtonProps } from '@material-ui/core/Button'; +import { CircularProgress, withStyles } from '@material-ui/core'; +import { CircularProgressProps } from '@material-ui/core/CircularProgress'; + +interface ProgressButtonProps extends ButtonProps { + loading?: boolean; + progressProps?: CircularProgressProps; +} + +export const ProgressButton = ({ loading, progressProps, children, disabled, ...props }: ProgressButtonProps) => + ; + +const Progress = withStyles({ + root: { + position: 'absolute', + }, +})(CircularProgress); + +const getProgressSize = (size?: 'small' | 'medium' | 'large') => { + switch (size) { + case 'small': + return 16; + case 'large': + return 24; + default: + return 20; + } +}; diff --git a/src/index.tsx b/src/index.tsx index ef658857..801a56a1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -50,6 +50,7 @@ import HTML5Backend from 'react-dnd-html5-backend'; import { initAdvanceFormProjectsTree } from '~/store/search-bar/search-bar-actions'; import { repositoryActionSet } from '~/views-components/context-menu/action-sets/repository-action-set'; import { sshKeyActionSet } from '~/views-components/context-menu/action-sets/ssh-key-action-set'; +import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions'; console.log(`Starting arvados [${getBuildInfo()}]`); @@ -88,6 +89,7 @@ fetchConfig() store.dispatch(setBuildInfo()); store.dispatch(setCurrentTokenDialogApiHost(apiHost)); store.dispatch(setUuidPrefix(config.uuidPrefix)); + store.dispatch(loadVocabulary); const TokenComponent = (props: any) => ; const MainPanelComponent = (props: any) => ; diff --git a/src/models/vocabulary.ts b/src/models/vocabulary.ts new file mode 100644 index 00000000..ea23ad2c --- /dev/null +++ b/src/models/vocabulary.ts @@ -0,0 +1,24 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { isObject, has, every } from 'lodash/fp'; + +export interface Vocabulary { + strict: boolean; + tags: Record; +} + +export interface Tag { + strict?: boolean; + values?: string[]; +} + +const VOCABULARY_VALIDATORS = [ + isObject, + has('strict'), + has('tags'), +]; + +export const isVocabulary = (value: any) => + every(validator => validator(value), VOCABULARY_VALIDATORS); \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index f1ef86b8..b24b1d99 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -27,6 +27,7 @@ import { PermissionService } from "~/services/permission-service/permission-serv import { VirtualMachinesService } from "~/services/virtual-machines-service/virtual-machines-service"; import { RepositoriesService } from '~/services/repositories-service/repositories-service'; import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service'; +import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service'; export type ServiceRepository = ReturnType; @@ -58,6 +59,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const favoriteService = new FavoriteService(linkService, groupsService); const tagService = new TagService(linkService); const searchService = new SearchService(); + const vocabularyService = new VocabularyService(config.vocabularyUrl); return { ancestorsService, @@ -82,6 +84,7 @@ export const createServices = (config: Config, actions: ApiActions) => { virtualMachineService, webdavClient, workflowService, + vocabularyService, }; }; diff --git a/src/services/vocabulary-service/vocabulary-service.ts b/src/services/vocabulary-service/vocabulary-service.ts new file mode 100644 index 00000000..57bdd7c9 --- /dev/null +++ b/src/services/vocabulary-service/vocabulary-service.ts @@ -0,0 +1,18 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import Axios from 'axios'; +import { Vocabulary } from '~/models/vocabulary'; + +export class VocabularyService { + constructor( + private url: string + ) { } + + getVocabulary() { + return Axios + .get(this.url) + .then(response => response.data); + } +} diff --git a/src/store/vocabulary/vocabulary-actions.ts b/src/store/vocabulary/vocabulary-actions.ts new file mode 100644 index 00000000..799cffa0 --- /dev/null +++ b/src/store/vocabulary/vocabulary-actions.ts @@ -0,0 +1,20 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { Dispatch } from 'redux'; +import { ServiceRepository } from '~/services/services'; +import { propertiesActions } from '~/store/properties/properties-actions'; +import { VOCABULARY_PROPERTY_NAME, DEFAULT_VOCABULARY } from './vocabulary-selctors'; +import { isVocabulary } from '~/models/vocabulary'; + +export const loadVocabulary = async (dispatch: Dispatch, _: {}, { vocabularyService }: ServiceRepository) => { + const vocabulary = await vocabularyService.getVocabulary(); + + dispatch(propertiesActions.SET_PROPERTY({ + key: VOCABULARY_PROPERTY_NAME, + value: isVocabulary(vocabulary) + ? vocabulary + : DEFAULT_VOCABULARY, + })); +}; diff --git a/src/store/vocabulary/vocabulary-selctors.ts b/src/store/vocabulary/vocabulary-selctors.ts new file mode 100644 index 00000000..d317cb47 --- /dev/null +++ b/src/store/vocabulary/vocabulary-selctors.ts @@ -0,0 +1,16 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { PropertiesState, getProperty } from '~/store/properties/properties'; +import { Vocabulary } from '~/models/vocabulary'; + +export const VOCABULARY_PROPERTY_NAME = 'vocabulary'; + +export const DEFAULT_VOCABULARY: Vocabulary = { + strict: false, + tags: {}, +}; + +export const getVocabulary = (state: PropertiesState) => + getProperty(VOCABULARY_PROPERTY_NAME)(state) || DEFAULT_VOCABULARY; diff --git a/src/views-components/project-properties-dialog/project-properties-form.tsx b/src/views-components/project-properties-dialog/project-properties-form.tsx index 82ae0406..90c8c080 100644 --- a/src/views-components/project-properties-dialog/project-properties-form.tsx +++ b/src/views-components/project-properties-dialog/project-properties-form.tsx @@ -2,94 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from 'react'; -import { reduxForm, Field, reset } from 'redux-form'; -import { compose, Dispatch } from 'redux'; -import { ArvadosTheme } from '~/common/custom-theme'; -import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core'; -import { TagProperty } from '~/models/tag'; -import { TextField } from '~/components/text-field/text-field'; -import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators'; +import { reduxForm, reset } from 'redux-form'; import { PROJECT_PROPERTIES_FORM_NAME, createProjectProperty } from '~/store/details-panel/details-panel-action'; +import { ResourcePropertiesForm, ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form'; +import { withStyles } from '@material-ui/core'; -type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress'; +const Form = withStyles(({ spacing }) => ({ container: { marginBottom: spacing.unit * 2 } }))(ResourcePropertiesForm); -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - root: { - width: '100%', - display: 'flex' - }, - keyField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - valueField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - buttonWrapper: { - paddingTop: '14px', - position: 'relative', - }, - saveButton: { - boxShadow: 'none' - }, - circularProgress: { - position: 'absolute', - top: -9, - bottom: 0, - left: 0, - right: 0, - margin: 'auto' +export const ProjectPropertiesForm = reduxForm({ + form: PROJECT_PROPERTIES_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(createProjectProperty(data)); + dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); } -}); - -interface ProjectPropertiesFormDataProps { - submitting: boolean; - invalid: boolean; - pristine: boolean; -} - -interface ProjectPropertiesFormActionProps { - handleSubmit: any; -} - -type ProjectPropertiesFormProps = ProjectPropertiesFormDataProps & ProjectPropertiesFormActionProps & WithStyles; - -export const ProjectPropertiesForm = compose( - reduxForm({ - form: PROJECT_PROPERTIES_FORM_NAME, - onSubmit: (data: TagProperty, dispatch: Dispatch) => { - dispatch(createProjectProperty(data)); - dispatch(reset(PROJECT_PROPERTIES_FORM_NAME)); - } - }), - withStyles(styles))( - ({ classes, submitting, pristine, invalid, handleSubmit }: ProjectPropertiesFormProps) => -
-
- -
-
- -
-
- - {submitting && } -
-
- ); +})(Form); diff --git a/src/views-components/resource-properties-form/property-field-common.tsx b/src/views-components/resource-properties-form/property-field-common.tsx new file mode 100644 index 00000000..028c46b9 --- /dev/null +++ b/src/views-components/resource-properties-form/property-field-common.tsx @@ -0,0 +1,45 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from 'react-redux'; +import { WrappedFieldMetaProps, WrappedFieldInputProps, WrappedFieldProps } from 'redux-form'; +import { identity } from 'lodash'; +import { Vocabulary } from '~/models/vocabulary'; +import { RootState } from '~/store/store'; +import { getVocabulary } from '~/store/vocabulary/vocabulary-selctors'; + +export interface VocabularyProp { + vocabulary: Vocabulary; +} + +export const mapStateToProps = (state: RootState): VocabularyProp => ({ + vocabulary: getVocabulary(state.properties), +}); + +export const connectVocabulary = connect(mapStateToProps); + +export const ITEMS_PLACEHOLDER: string[] = []; + +export const hasError = ({ touched, invalid }: WrappedFieldMetaProps) => + touched && invalid; + +export const getErrorMsg = (meta: WrappedFieldMetaProps) => + hasError(meta) + ? meta.error + : ''; + +export const handleBlur = ({ onBlur, value }: WrappedFieldInputProps) => + () => + onBlur(value); + +export const buildProps = ({ input, meta }: WrappedFieldProps) => ({ + value: input.value, + onChange: input.onChange, + onBlur: handleBlur(input), + items: ITEMS_PLACEHOLDER, + onSelect: input.onChange, + renderSuggestion: identity, + error: hasError(meta), + helperText: getErrorMsg(meta), +}); diff --git a/src/views-components/resource-properties-form/property-key-field.tsx b/src/views-components/resource-properties-form/property-key-field.tsx new file mode 100644 index 00000000..e6708a39 --- /dev/null +++ b/src/views-components/resource-properties-form/property-key-field.tsx @@ -0,0 +1,46 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { WrappedFieldProps, Field } from 'redux-form'; +import { memoize } from 'lodash'; +import { Autocomplete } from '~/components/autocomplete/autocomplete'; +import { Vocabulary } from '~/models/vocabulary'; +import { connectVocabulary, VocabularyProp, buildProps } from '~/views-components/resource-properties-form/property-field-common'; +import { TAG_KEY_VALIDATION } from '~/validators/validators'; + +export const PROPERTY_KEY_FIELD_NAME = 'key'; + +export const PropertyKeyField = connectVocabulary( + ({ vocabulary }: VocabularyProp) => + ); + +const PropertyKeyInput = ({ vocabulary, ...props }: WrappedFieldProps & VocabularyProp) => + ; + +const getValidation = memoize( + (vocabulary: Vocabulary) => + vocabulary.strict + ? [...TAG_KEY_VALIDATION, matchTags(vocabulary)] + : TAG_KEY_VALIDATION); + +const matchTags = (vocabulary: Vocabulary) => + (value: string) => + getTagsList(vocabulary).find(tag => tag.includes(value)) + ? undefined + : 'Incorrect key'; + +const getSuggestions = (value: string, vocabulary: Vocabulary) => + getTagsList(vocabulary).filter(tag => tag.includes(value) && tag !== value); + +const getTagsList = ({ tags }: Vocabulary) => + Object.keys(tags); diff --git a/src/views-components/resource-properties-form/property-value-field.tsx b/src/views-components/resource-properties-form/property-value-field.tsx new file mode 100644 index 00000000..db2db3f7 --- /dev/null +++ b/src/views-components/resource-properties-form/property-value-field.tsx @@ -0,0 +1,62 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { WrappedFieldProps, Field, formValues } from 'redux-form'; +import { compose } from 'redux'; +import { Autocomplete } from '~/components/autocomplete/autocomplete'; +import { Vocabulary } from '~/models/vocabulary'; +import { PROPERTY_KEY_FIELD_NAME } from '~/views-components/resource-properties-form/property-key-field'; +import { VocabularyProp, connectVocabulary, buildProps } from '~/views-components/resource-properties-form/property-field-common'; +import { TAG_VALUE_VALIDATION } from '~/validators/validators'; + +interface PropertyKeyProp { + propertyKey: string; +} + +type PropertyValueFieldProps = VocabularyProp & PropertyKeyProp; + +export const PROPERTY_VALUE_FIELD_NAME = 'value'; + +export const PropertyValueField = compose( + connectVocabulary, + formValues({ propertyKey: PROPERTY_KEY_FIELD_NAME }) +)( + (props: PropertyValueFieldProps) => + ); + +const PropertyValueInput = ({ vocabulary, propertyKey, ...props }: WrappedFieldProps & PropertyValueFieldProps) => + ; + +const getValidation = (props: PropertyValueFieldProps) => + isStrictTag(props.propertyKey, props.vocabulary) + ? [...TAG_VALUE_VALIDATION, matchTagValues(props)] + : TAG_VALUE_VALIDATION; + +const matchTagValues = ({ vocabulary, propertyKey }: PropertyValueFieldProps) => + (value: string) => + getTagValues(propertyKey, vocabulary).find(v => v.includes(value)) + ? undefined + : 'Incorrect value'; + +const getSuggestions = (value: string, tagName: string, vocabulary: Vocabulary) => + getTagValues(tagName, vocabulary).filter(v => v.includes(value) && v !== value); + +const isStrictTag = (tagName: string, vocabulary: Vocabulary) => { + const tag = vocabulary.tags[tagName]; + return tag ? tag.strict : false; +}; + +const getTagValues = (tagName: string, vocabulary: Vocabulary) => { + const tag = vocabulary.tags[tagName]; + return tag && tag.values ? tag.values : []; +}; diff --git a/src/views-components/resource-properties-form/resource-properties-form.tsx b/src/views-components/resource-properties-form/resource-properties-form.tsx new file mode 100644 index 00000000..a62b3d15 --- /dev/null +++ b/src/views-components/resource-properties-form/resource-properties-form.tsx @@ -0,0 +1,44 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { InjectedFormProps } from 'redux-form'; +import { Grid, withStyles, WithStyles } from '@material-ui/core'; +import { PropertyKeyField, PROPERTY_KEY_FIELD_NAME } from './property-key-field'; +import { PropertyValueField, PROPERTY_VALUE_FIELD_NAME } from './property-value-field'; +import { ProgressButton } from '~/components/progress-button/progress-button'; +import { GridClassKey } from '@material-ui/core/Grid'; + +export interface ResourcePropertiesFormData { + [PROPERTY_KEY_FIELD_NAME]: string; + [PROPERTY_VALUE_FIELD_NAME]: string; +} + +export type ResourcePropertiesFormProps = InjectedFormProps & WithStyles; + +export const ResourcePropertiesForm = ({ handleSubmit, submitting, invalid, classes }: ResourcePropertiesFormProps ) => +
+ + + + + + + + + + + +
; + +const Button = withStyles(theme => ({ + root: { marginTop: theme.spacing.unit } +}))(ProgressButton); diff --git a/src/views/collection-panel/collection-tag-form.tsx b/src/views/collection-panel/collection-tag-form.tsx index 9aa88128..fd4f0880 100644 --- a/src/views/collection-panel/collection-tag-form.tsx +++ b/src/views/collection-panel/collection-tag-form.tsx @@ -2,103 +2,17 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from 'react'; -import { reduxForm, Field, reset } from 'redux-form'; -import { compose, Dispatch } from 'redux'; -import { ArvadosTheme } from '~/common/custom-theme'; -import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress, Grid, Typography } from '@material-ui/core'; -import { TagProperty } from '~/models/tag'; -import { TextField } from '~/components/text-field/text-field'; +import { reduxForm, reset } from 'redux-form'; import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '~/store/collection-panel/collection-panel-action'; -import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators'; +import { ResourcePropertiesForm, ResourcePropertiesFormData } from '~/views-components/resource-properties-form/resource-properties-form'; +import { withStyles } from '@material-ui/core'; -type CssRules = 'root' | 'keyField' | 'valueField' | 'buttonWrapper' | 'saveButton' | 'circularProgress'; +const Form = withStyles(({ spacing }) => ({ container: { marginBottom: spacing.unit * 2 } }))(ResourcePropertiesForm); -const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - root: { - width: '100%', - display: 'flex' - }, - keyField: { - width: '25%', - marginRight: theme.spacing.unit * 3 - }, - valueField: { - width: '40%', - marginRight: theme.spacing.unit * 3 - }, - buttonWrapper: { - paddingTop: '14px', - position: 'relative', - }, - saveButton: { - boxShadow: 'none' - }, - circularProgress: { - position: 'absolute', - top: -9, - bottom: 0, - left: 0, - right: 0, - margin: 'auto' +export const CollectionTagForm = reduxForm({ + form: COLLECTION_TAG_FORM_NAME, + onSubmit: (data, dispatch) => { + dispatch(createCollectionTag(data)); + dispatch(reset(COLLECTION_TAG_FORM_NAME)); } -}); - -interface CollectionTagFormDataProps { - submitting: boolean; - invalid: boolean; - pristine: boolean; -} - -interface CollectionTagFormActionProps { - handleSubmit: any; -} - -type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles; - -export const CollectionTagForm = compose( - reduxForm({ - form: COLLECTION_TAG_FORM_NAME, - onSubmit: (data: TagProperty, dispatch: Dispatch) => { - dispatch(createCollectionTag(data)); - dispatch(reset(COLLECTION_TAG_FORM_NAME)); - } - }), - withStyles(styles))( - - class CollectionTagForm extends React.Component { - - render() { - const { classes, submitting, pristine, invalid, handleSubmit } = this.props; - return ( -
-
- -
-
- -
-
- - {submitting && } -
-
- ); - } - } - - ); +})(Form);