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 (
+