diff --git a/src/components/common/ConceptContainerForm.jsx b/src/components/common/ConceptContainerForm.jsx new file mode 100644 index 00000000..55ecb650 --- /dev/null +++ b/src/components/common/ConceptContainerForm.jsx @@ -0,0 +1,541 @@ +import React from 'react'; +import alertifyjs from 'alertifyjs'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import { Add as AddIcon } from '@material-ui/icons'; +import { + TextField, IconButton, Button, CircularProgress, Select, MenuItem, FormControl, InputLabel, +} from '@material-ui/core'; +import { + set, get, map, cloneDeep, pullAt, isEmpty, startCase, pickBy, isObject, isArray, + find, intersectionBy +} from 'lodash'; +import APIService from '../../services/APIService'; +import { arrayToObject, getCurrentURL, fetchLocales } from '../../common/utils'; +import ExtrasForm from '../common/ExtrasForm'; +const JSON_MODEL = {key: '', value: ''} + + +// props => resource, resourceType (source), defaultIdText (SourceCode), urlPath (sources), +// props => types, placeholders ({id: ''}) +// source => content_type, source_type +// collection => immutable, collection_type +class ConceptContainerForm extends React.Component { + constructor(props) { + super(props); + this.state = { + fields: { + id: '', + name: '', + full_name: '', + website: '', + source_type: '', + collection_type: '', + public_access: 'View', + external_id: '', + default_locale: '', + supported_locales: '', + custom_validation_schema: 'None', + description: '', + extras: [cloneDeep(JSON_MODEL)], + canonical_url: '', + publisher: '', + purpose: '', + copyright: '', + content_type: '', + revision_date: '', //date + identifier: '', //json + contact: '', //json + jurisdiction: '', //json + }, + typeAttr: '', + fieldErrors: {}, + serverErrors: null, + selected_source_type: null, + selected_collection_type: null, + locales: [], + } + } + + componentDidMount() { + fetchLocales(locales => this.setState({locales: locales})) + const { edit, resource } = this.props + + this.setState({typeAttr: this.isSource() ? 'source_type' : 'collection_type'}, () => { + if(edit && resource) + this.setFieldsForEdit() + }) + } + + isSource() { + return get(this.props, 'resource.type') === 'Source' || this.props.resourceType === 'source' + } + + componentDidUpdate() { + this.setSupportedLocales() + } + + setSupportedLocales() { + this.setSupportedLocalesValidation(); + this.setSupportedLocalesDisplay(); + } + + setSupportedLocalesValidation() { + const el = document.getElementById('fields.supported_locales') + if(!el) return; + if(this.state.fields.supported_locales) + el.removeAttribute('required') + else + el.setAttribute('required', 'true') + } + + setSupportedLocalesDisplay() { + if(this.props.edit && this.props.resource && !isEmpty(this.state.locales)) { + if(find(this.state.selected_supported_locales, local => local.name.indexOf('[') === -1)) { + const newState = {...this.state} + newState.selected_supported_locales = intersectionBy(this.state.locales, newState.selected_supported_locales, 'id') + this.setState(newState) + } + } + } + + setFieldsForEdit() { + const { resource } = this.props; + const { typeAttr } = this.state; + const attrs = [ + 'id', 'external_id', 'name', 'full_name', 'description', 'revision_date', + 'content_type', 'copyright', 'purpose', 'publisher', 'canonical_url', 'description', + 'custom_validation_schema', 'public_access', 'website', 'default_locale' + ] + const jsonAttrs = ['jurisdiction', 'contact', 'identifier'] + const newState = {...this.state} + attrs.forEach(attr => set(newState.fields, attr, get(resource, attr, '') || '')) + jsonAttrs.forEach(attr => this.setJSONValue(resource, newState, attr)) + newState.fields.supported_locales = isArray(resource.supported_locales) ? resource.supported_locales.join(',') : resource.supported_locales; + + newState.custom_validation_schema = get(resource, 'custom_validation_schema') || 'None'; + newState.fields[typeAttr] = get(resource, typeAttr, '') || ''; + newState[`selected_${typeAttr}`] = {id: resource[typeAttr], name: resource[typeAttr]} + newState.selected_default_locale = {id: resource.default_locale, name: resource.default_locale} + newState.selected_supported_locales = map(resource.supported_locales, l => ({id: l, name: l})) + newState.fields.extras = isEmpty(resource.extras) ? newState.fields.extras : map(resource.extras, (v, k) => ({key: k, value: v})) + this.setState(newState, this.setSupportedLocales); + } + + setJSONValue(resource, state, field) { + const value = get(resource, field); + if(isEmpty(value)) + return + + if(isObject(value)) set(state.fields, field, JSON.stringify(value)) + else set(state.fields, field, value) + } + + getIdHelperText() { + const { defaultIdText, resourceType, urlPath } = this.props + const id = this.state.fields.id || `[${defaultIdText}]` + return ( + + Allowed characters are : Alphabets(a-z,A-Z), Numbers(0-9) and Hyphen(-). +
+ + {`Your new ${resourceType} will live at: `}
+ { + `${getCurrentURL()}/${urlPath}/` + } +
+ {id}/ +
+
+ ) + } + + onTextFieldChange = event => { + this.setFieldValue(event.target.id, event.target.value) + } + + onAutoCompleteChange = (id, item) => { + this.setFieldValue(id, get(item, 'id', ''), true) + } + + onMultiAutoCompleteChange = (event, items) => { + this.setFieldValue('fields.supported_locales', map(items, 'id').join(',')) + this.setFieldValue('selected_supported_locales', items) + } + + setFieldValue(id, value, setObject=false) { + const newState = {...this.state} + set(newState, id, value) + + const fieldName = get(id.split('fields.'), '1') + if(fieldName && !isEmpty(value) && get(newState.fieldErrors, fieldName)) + newState.fieldErrors[fieldName] = null + if(setObject) + newState[`selected_${fieldName}`] = {id: value, name: value} + this.setState(newState) + } + + onSubmit = event => { + event.preventDefault(); + event.stopPropagation(); + + const { parentURL, edit, urlPath } = this.props + const form = document.getElementsByTagName('form')[0]; + form.reportValidity() + const isFormValid = form.checkValidity() + if(parentURL && isFormValid) { + const fields = this.getPayload() + const service = APIService.new().overrideURL(parentURL) + if(edit) + service.put(fields).then(response => this.handleSubmitResponse(response)) + else + service.appendToUrl(`${urlPath}/`).post(fields).then(response => this.handleSubmitResponse(response)) + } + } + + getPayload() { + let fields = cloneDeep(this.state.fields); + + if(this.isSource()) { + delete fields.collection_type; + delete fields.immutable; + } else { + delete fields.content_type; + delete fields.source_type; + } + + fields.extras = arrayToObject(fields.extras) + + if(this.props.edit) + fields.update_comment = fields.comment + fields = pickBy(fields, value => value) + + return fields + } + + handleSubmitResponse(response) { + const { edit, reloadOnSuccess, onCancel, resourceType } = this.props + if(response.status === 201 || response.status === 200) { // success + const verb = edit ? 'updated' : 'created' + const successMsg = `Successfully ${verb} ${resourceType}`; + const message = reloadOnSuccess ? successMsg + '. Reloading..' : successMsg; + onCancel(); + alertifyjs.success(message, 1, () => { + if(reloadOnSuccess) + window.location.reload() + }) + } else { // error + const genericError = get(response, '__all__') + if(genericError) { + alertifyjs.error(genericError.join('\n')) + } else { + this.setState( + {fieldErrors: response || {}}, + () => alertifyjs.error('Please fill mandatory fields.') + ) + } + } + } + + onAddExtras = () => { + this.setFieldValue('fields.extras', [...this.state.fields.extras, cloneDeep(JSON_MODEL)]) + } + + onDeleteExtras = index => { + const newState = {...this.state} + pullAt(newState.fields.extras, index) + this.setState(newState) + } + + onExtrasChange = (index, key, value) => { + const newState = {...this.state} + if(key !== '__') + newState.fields.extras[index].key = key + if(value !== '__') + newState.fields.extras[index].value = value + this.setState(newState) + } + + render() { + const { + fields, fieldErrors, locales, selected_default_locale, selected_supported_locales, typeAttr, + selected_source_type, selected_collection_type, + } = this.state; + const { onCancel, edit, types, resourceType, placeholders, extraFields } = this.props; + const isSource = this.isSource() + const selected_type = isSource ? selected_source_type : selected_collection_type; + const isLoading = isEmpty(locales); + const resourceTypeLabel = startCase(resourceType) + const header = edit ? `Edit ${resourceTypeLabel}: ${fields.id}` : `New ${resourceTypeLabel}` + return ( +
+
+

{header}

+
+ { + isLoading ? +
+ +
: +
+
+ { + !edit && +
+ +
+ } +
+ +
+
+ +
+
+ +
+
+ +
+
+ option.id === get(value, 'id')} + value={selected_type} + id={`fields.${typeAttr}`} + options={types} + getOptionLabel={(option) => option.name} + fullWidth + required + renderInput={ + params => + } + onChange={(event, item) => this.onAutoCompleteChange(`fields.${typeAttr}`, item)} + /> +
+
+ + Public Access + + +
+
+ option.id === get(value, 'id')} + value={selected_default_locale} + id="fields.default_locale" + options={locales} + getOptionLabel={(option) => option.name} + fullWidth + required + renderInput={ + params => + } + onChange={(event, item) => this.onAutoCompleteChange('fields.default_locale', item)} + /> +
+
+ option.id === get(value, 'id')} + value={selected_supported_locales} + id="fields.supported_locales" + options={locales} + getOptionLabel={(option) => option.name} + fullWidth + required + renderInput={ + params => + } + onChange={this.onMultiAutoCompleteChange} + /> +
+
+ + Custom Validation Schema + + +
+
+ +
+
+ +
+ { + map(extraFields, attr => ( +
+ +
+ )) + } +
+ +
+
+
+

Custom Attributes

+
+
+ + + +
+ { + map(fields.extras, (extra, index) => ( +
0 ? {marginTop: '5px', width: '100%'} : {width: '100%'}}> + +
+ )) + } +
+
+ + +
+
+
+ } +
+ ) + } +} + +export default ConceptContainerForm; diff --git a/src/components/sources/SourceForm.jsx b/src/components/sources/SourceForm.jsx index 6a2d2d6f..095f485b 100644 --- a/src/components/sources/SourceForm.jsx +++ b/src/components/sources/SourceForm.jsx @@ -1,504 +1,28 @@ import React from 'react'; -import alertifyjs from 'alertifyjs'; -import Autocomplete from '@material-ui/lab/Autocomplete'; -import { Add as AddIcon } from '@material-ui/icons'; -import { - TextField, IconButton, Button, CircularProgress, Select, MenuItem, FormControl, InputLabel, -} from '@material-ui/core'; -import { - set, get, map, orderBy, cloneDeep, pullAt, isEmpty, startCase, pickBy, isObject, isArray, - find, intersectionBy -} from 'lodash'; -import APIService from '../../services/APIService'; -import { arrayToObject, getCurrentURL, fetchLocales } from '../../common/utils'; +import { orderBy, map } from 'lodash'; import { SOURCE_TYPES } from '../../common/constants'; -import ExtrasForm from '../common/ExtrasForm'; -const JSON_MODEL = {key: '', value: ''} - -class SourceForm extends React.Component { - constructor(props) { - super(props); - this.state = { - fields: { - id: '', - name: '', - full_name: '', - website: '', - source_type: '', - public_access: 'View', - external_id: '', - default_locale: '', - supported_locales: '', - custom_validation_schema: 'None', - description: '', - extras: [cloneDeep(JSON_MODEL)], - canonical_url: '', - publisher: '', - purpose: '', - copyright: '', - content_type: '', - revision_date: '', //date - identifier: '', //json - contact: '', //json - jurisdiction: '', //json - }, - fieldErrors: {}, - serverErrors: null, - selected_source_type: null, - locales: [], - sourceTypes: orderBy(map(SOURCE_TYPES, t => ({id: t, name: t})), 'name') - } - } - - componentDidMount() { - fetchLocales(locales => this.setState({locales: locales})) - if(this.props.edit && this.props.source) - this.setFieldsForEdit() - } - - componentDidUpdate() { - this.setSupportedLocales() - } - - setSupportedLocales() { - this.setSupportedLocalesValidation(); - this.setSupportedLocalesDisplay(); - } - - setSupportedLocalesValidation() { - const el = document.getElementById('fields.supported_locales') - if(!el) return; - if(this.state.fields.supported_locales) - el.removeAttribute('required') - else - el.setAttribute('required', 'true') - } - - setSupportedLocalesDisplay() { - if(this.props.edit && this.props.source && !isEmpty(this.state.locales)) { - if(find(this.state.selected_supported_locales, local => local.name.indexOf('[') === -1)) { - const newState = {...this.state} - newState.selected_supported_locales = intersectionBy(this.state.locales, newState.selected_supported_locales, 'id') - this.setState(newState) - } - } - } - - setFieldsForEdit() { - const { source } = this.props; - const attrs = [ - 'id', 'source_type', 'external_id', 'name', 'full_name', 'description', 'revision_date', - 'content_type', 'copyright', 'purpose', 'publisher', 'canonical_url', 'description', - 'custom_validation_schema', 'public_access', 'website', 'default_locale' - ] - const jsonAttrs = ['jurisdiction', 'contact', 'identifier'] - const newState = {...this.state} - attrs.forEach(attr => set(newState.fields, attr, get(source, attr, '') || '')) - jsonAttrs.forEach(attr => this.setJSONValue(source, newState, attr)) - newState.fields.supported_locales = isArray(source.supported_locales) ? source.supported_locales.join(',') : source.supported_locales; - newState.selected_source_type = {id: source.source_type, name: source.source_type} - newState.selected_default_locale = {id: source.default_locale, name: source.default_locale} - newState.selected_supported_locales = map(source.supported_locales, l => ({id: l, name: l})) - newState.fields.extras = isEmpty(source.extras) ? newState.fields.extras : map(source.extras, (v, k) => ({key: k, value: v})) - this.setState(newState, this.setSupportedLocales); - } - - setJSONValue(source, state, field) { - const value = get(source, field); - if(isEmpty(value)) - return - - if(isObject(value)) set(state.fields, field, JSON.stringify(value)) - else set(state.fields, field, value) - } - - getIdHelperText() { - const id = this.state.fields.id || "[SourceCode]" - return ( - - Allowed characters are : Alphabets(a-z,A-Z), Numbers(0-9) and Hyphen(-). -
- - Your new source will live at:
- { - `${getCurrentURL()}/sources/` - } -
- {id}/ -
-
- ) - } - - onTextFieldChange = event => { - this.setFieldValue(event.target.id, event.target.value) - } - - onAutoCompleteChange = (id, item) => { - this.setFieldValue(id, get(item, 'id', ''), true) - } - - onMultiAutoCompleteChange = (event, items) => { - this.setFieldValue('fields.supported_locales', map(items, 'id').join(',')) - this.setFieldValue('selected_supported_locales', items) - } - - setFieldValue(id, value, setObject=false) { - const newState = {...this.state} - set(newState, id, value) - - const fieldName = get(id.split('fields.'), '1') - if(fieldName && !isEmpty(value) && get(newState.fieldErrors, fieldName)) - newState.fieldErrors[fieldName] = null - if(setObject) - newState[`selected_${fieldName}`] = {id: value, name: value} - this.setState(newState) - } - - onSubmit = event => { - event.preventDefault(); - event.stopPropagation(); - - const { parentURL, edit } = this.props - let fields = cloneDeep(this.state.fields); - const form = document.getElementsByTagName('form')[0]; - form.reportValidity() - const isFormValid = form.checkValidity() - if(parentURL && isFormValid) { - fields.extras = arrayToObject(fields.extras) - if(edit) - fields.update_comment = fields.comment - fields = pickBy(fields, value => value) - let service = APIService.new().overrideURL(parentURL) - if(edit) { - service.put(fields).then(response => this.handleSubmitResponse(response)) - } else { - service.appendToUrl('sources/').post(fields).then(response => this.handleSubmitResponse(response)) - } - } - } - - handleSubmitResponse(response) { - const { edit, reloadOnSuccess, onCancel } = this.props - if(response.status === 201 || response.status === 200) { // success - const verb = edit ? 'updated' : 'created' - const successMsg = `Successfully ${verb} source`; - const message = reloadOnSuccess ? successMsg + '. Reloading..' : successMsg; - onCancel(); - alertifyjs.success(message, 1, () => { - if(reloadOnSuccess) - window.location.reload() - }) - } else { // error - const genericError = get(response, '__all__') - if(genericError) { - alertifyjs.error(genericError.join('\n')) - } else { - this.setState( - {fieldErrors: response || {}}, - () => alertifyjs.error('Please fill mandatory fields.') - ) - } - } - } - - onAddExtras = () => { - this.setFieldValue('fields.extras', [...this.state.fields.extras, cloneDeep(JSON_MODEL)]) - } - - onDeleteExtras = index => { - const newState = {...this.state} - pullAt(newState.fields.extras, index) - this.setState(newState) - } - - onExtrasChange = (index, key, value) => { - const newState = {...this.state} - if(key !== '__') - newState.fields.extras[index].key = key - if(value !== '__') - newState.fields.extras[index].value = value - this.setState(newState) - } +import ConceptContainerForm from '../common/ConceptContainerForm'; + +const CONFIGS = { + resourceType: 'source', + defaultIdText: 'SourceCode', + urlPath: 'sources', + types: orderBy(map(SOURCE_TYPES, t => ({id: t, name: t})), 'name'), + placeholders: { + id: "e.g. ICD-10", + name: "e.g. ICD 10", + full_name: "e.g. International Classification for Diseases v10", + website: "e.g. http://apps.who.int/classifications/icd10/", + external_id: "e.g. UUID from external system", + canonical_url: "e.g. http://who.int/ICPC-2", + }, + extraFields: ['publisher', 'purpose', 'copyright', 'content_type', 'identifier', 'contact', 'jurisdiction'] +} - render() { - const { - fields, fieldErrors, locales, sourceTypes, - selected_default_locale, selected_supported_locales, selected_source_type - } = this.state; - const { onCancel, edit } = this.props; - const isLoading = isEmpty(locales); - const header = edit ? `Edit Source: ${fields.id}` : 'New Source' - return ( -
-
-

{header}

-
- { - isLoading ? -
- -
: -
-
- { - !edit && -
- -
- } -
- -
-
- -
-
- -
-
- -
-
- option.id === get(value, 'id')} - value={selected_source_type} - id="fields.source_type" - options={sourceTypes} - getOptionLabel={(option) => option.name} - fullWidth - required - renderInput={ - params => - } - onChange={(event, item) => this.onAutoCompleteChange('fields.source_type', item)} - /> -
-
- - Public Access - - -
-
- option.id === get(value, 'id')} - value={selected_default_locale} - id="fields.default_locale" - options={locales} - getOptionLabel={(option) => option.name} - fullWidth - required - renderInput={ - params => - } - onChange={(event, item) => this.onAutoCompleteChange('fields.default_locale', item)} - /> -
-
- option.id === get(value, 'id')} - value={selected_supported_locales} - id="fields.supported_locales" - options={locales} - getOptionLabel={(option) => option.name} - fullWidth - required - renderInput={ - params => - } - onChange={this.onMultiAutoCompleteChange} - /> -
-
- - Custom Validation Schema - - -
-
- -
-
- -
- { - map(['publisher', 'purpose', 'copyright', 'content_type', 'identifier', 'contact', 'jurisdiction'], attr => ( -
- -
- )) - } -
- -
-
-
-

Custom Attributes

-
-
- - - -
- { - map(fields.extras, (extra, index) => ( -
0 ? {marginTop: '5px', width: '100%'} : {width: '100%'}}> - -
- )) - } -
-
- - -
-
-
- } -
- ) - } +const SourceForm = props => { + return ( + + ) } export default SourceForm;