{ item.sortable ?
- if (sortClasses === 'icon-up') {
-
- } else {
-
- }
+ { !desc && }
+ { !!desc && }
{item.headerLabel}
: item.headerLabel
diff --git a/src/config/constants.js b/src/config/constants.js
index 5abfd9474..9f56e276c 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -399,6 +399,66 @@ export const LOAD_PROJECTS_METADATA_PENDING = 'LOAD_PROJECTS_METADATA_PENDING'
export const LOAD_PROJECTS_METADATA_SUCCESS = 'LOAD_PROJECTS_METADATA_SUCCESS'
export const LOAD_PROJECTS_METADATA_FAILURE = 'LOAD_PROJECTS_METADATA_FAILURE'
+export const ADD_PROJECTS_METADATA = 'ADD_PROJECT_METADATA'
+export const ADD_PROJECTS_METADATA_PENDING = 'ADD_PROJECT_METADATA_PENDING'
+export const ADD_PROJECTS_METADATA_SUCCESS = 'ADD_PROJECT_METADATA_SUCCESS'
+export const ADD_PROJECTS_METADATA_FAILURE = 'ADD_PROJECT_METADATA_FAILURE'
+
+export const CREATE_PROJECT_TEMPLATE = 'CREATE_PROJECT_TEMPLATE'
+export const CREATE_PROJECT_TEMPLATE_PENDING = 'CREATE_PROJECT_TEMPLATE_PENDING'
+export const CREATE_PROJECT_TEMPLATE_SUCCESS = 'CREATE_PROJECT_TEMPLATE_SUCCESS'
+export const CREATE_PROJECT_TEMPLATE_FAILURE = 'CREATE_PROJECT_TEMPLATE_FAILURE'
+
+export const CREATE_PRODUCT_TEMPLATE = 'CREATE_PRODUCT_TEMPLATE'
+export const CREATE_PRODUCT_TEMPLATE_PENDING = 'CREATE_PRODUCT_TEMPLATE_PENDING'
+export const CREATE_PRODUCT_TEMPLATE_SUCCESS = 'CREATE_PRODUCT_TEMPLATE_SUCCESS'
+export const CREATE_PRODUCT_TEMPLATE_FAILURE = 'CREATE_PRODUCT_TEMPLATE_FAILURE'
+
+export const CREATE_PROJECT_TYPE = 'CREATE_PROJECT_TYPE'
+export const CREATE_PROJECT_TYPE_PENDING = 'CREATE_PROJECT_TYPE_PENDING'
+export const CREATE_PROJECT_TYPE_SUCCESS = 'CREATE_PROJECT_TYPE_SUCCESS'
+export const CREATE_PROJECT_TYPE_FAILURE = 'CREATE_PROJECT_TYPE_FAILURE'
+
+export const CREATE_PRODUCT_CATEGORY = 'CREATE_PRODUCT_CATEGORY'
+export const CREATE_PRODUCT_CATEGORY_PENDING = 'CREATE_PRODUCT_CATEGORY_PENDING'
+export const CREATE_PRODUCT_CATEGORY_SUCCESS = 'CREATE_PRODUCT_CATEGORY_SUCCESS'
+export const CREATE_PRODUCT_CATEGORY_FAILURE = 'CREATE_PRODUCT_CATEGORY_FAILURE'
+
+export const UPDATE_PROJECTS_METADATA = 'UPDATE_PROJECT_METADATA'
+export const UPDATE_PROJECTS_METADATA_PENDING = 'UPDATE_PROJECT_METADATA_PENDING'
+export const UPDATE_PROJECTS_METADATA_SUCCESS = 'UPDATE_PROJECT_METADATA_SUCCESS'
+export const UPDATE_PROJECTS_METADATA_FAILURE = 'UPDATE_PROJECT_METADATA_FAILURE'
+
+export const REMOVE_PROJECTS_METADATA = 'REMOVE_PROJECT_METADATA'
+export const REMOVE_PROJECTS_METADATA_PENDING = 'REMOVE_PROJECT_METADATA_PENDING'
+export const REMOVE_PROJECTS_METADATA_SUCCESS = 'REMOVE_PROJECT_METADATA_SUCCESS'
+export const REMOVE_PROJECTS_METADATA_FAILURE = 'REMOVE_PROJECT_METADATA_FAILURE'
+
+export const REMOVE_PRODUCT_TEMPLATE = 'REMOVE_PRODUCT_TEMPLATE'
+export const REMOVE_PRODUCT_TEMPLATE_PENDING = 'REMOVE_PRODUCT_TEMPLATE_PENDING'
+export const REMOVE_PRODUCT_TEMPLATE_SUCCESS = 'REMOVE_PRODUCT_TEMPLATE_SUCCESS'
+export const REMOVE_PRODUCT_TEMPLATE_FAILURE = 'REMOVE_PRODUCT_TEMPLATE_FAILURE'
+
+export const REMOVE_PRODUCT_CATEGORY = 'REMOVE_PRODUCT_CATEGORY'
+export const REMOVE_PRODUCT_CATEGORY_PENDING = 'REMOVE_PRODUCT_CATEGORY_PENDING'
+export const REMOVE_PRODUCT_CATEGORY_SUCCESS = 'REMOVE_PRODUCT_CATEGORY_SUCCESS'
+export const REMOVE_PRODUCT_CATEGORY_FAILURE = 'REMOVE_PRODUCT_CATEGORY_FAILURE'
+
+export const REMOVE_PROJECT_TEMPLATE = 'REMOVE_PROJECT_TEMPLATE'
+export const REMOVE_PROJECT_TEMPLATE_PENDING = 'REMOVE_PROJECT_TEMPLATE_PENDING'
+export const REMOVE_PROJECT_TEMPLATE_SUCCESS = 'REMOVE_PROJECT_TEMPLATE_SUCCESS'
+export const REMOVE_PROJECT_TEMPLATE_FAILURE = 'REMOVE_PROJECT_TEMPLATE_FAILURE'
+
+export const REMOVE_PROJECT_TYPE = 'REMOVE_PROJECT_TYPE'
+export const REMOVE_PROJECT_TYPE_PENDING = 'REMOVE_PROJECT_TYPE_PENDING'
+export const REMOVE_PROJECT_TYPE_SUCCESS = 'REMOVE_PROJECT_TYPE_SUCCESS'
+export const REMOVE_PROJECT_TYPE_FAILURE = 'REMOVE_PROJECT_TYPE_FAILURE'
+
+export const PROJECT_TEMPLATES_SORT = 'PROJECT_TEMPLATES_SORT'
+export const PRODUCT_TEMPLATES_SORT = 'PRODUCT_TEMPLATES_SORT'
+export const PROJECT_TYPES_SORT = 'PROJECT_TYPES_SORT'
+export const PRODUCT_CATEGORIES_SORT = 'PRODUCT_CATEGORIES_SORT'
+
export const THREAD_MESSAGES_PAGE_SIZE = 3
/*
* Project status
diff --git a/src/projects/list/components/Projects/ProjectsGridView.jsx b/src/projects/list/components/Projects/ProjectsGridView.jsx
index 4a8b45b40..1f3a55c59 100644
--- a/src/projects/list/components/Projects/ProjectsGridView.jsx
+++ b/src/projects/list/components/Projects/ProjectsGridView.jsx
@@ -220,8 +220,10 @@ const ProjectsGridView = props => {
infiniteAutoload,
infiniteScroll: true,
setInfiniteAutoload,
- projectsStatus,
- applyFilters
+ applyFilters,
+ entityName: 'project',
+ entityNamePlural: 'projects',
+ noMoreResultsMessage: `No more ${projectsStatus} projects`
}
return (
diff --git a/src/reducers/templates.js b/src/reducers/templates.js
index a49c2adde..62904eda0 100644
--- a/src/reducers/templates.js
+++ b/src/reducers/templates.js
@@ -1,14 +1,55 @@
+
+import _ from 'lodash'
+import update from 'react-addons-update'
import {
LOAD_PROJECTS_METADATA_PENDING,
LOAD_PROJECTS_METADATA_SUCCESS,
+ ADD_PROJECTS_METADATA_PENDING,
+ UPDATE_PROJECTS_METADATA_PENDING,
+ REMOVE_PROJECTS_METADATA_PENDING,
+ ADD_PROJECTS_METADATA_FAILURE,
+ UPDATE_PROJECTS_METADATA_FAILURE,
+ REMOVE_PROJECTS_METADATA_FAILURE,
+ ADD_PROJECTS_METADATA_SUCCESS,
+ UPDATE_PROJECTS_METADATA_SUCCESS,
+ REMOVE_PROJECTS_METADATA_SUCCESS,
+ PROJECT_TEMPLATES_SORT,
+ PRODUCT_TEMPLATES_SORT,
+ PROJECT_TYPES_SORT,
+ CREATE_PROJECT_TEMPLATE_PENDING,
+ CREATE_PROJECT_TEMPLATE_FAILURE,
+ CREATE_PROJECT_TEMPLATE_SUCCESS,
+ CREATE_PRODUCT_TEMPLATE_PENDING,
+ CREATE_PROJECT_TYPE_PENDING,
+ CREATE_PRODUCT_TEMPLATE_FAILURE,
+ CREATE_PROJECT_TYPE_FAILURE,
+ CREATE_PRODUCT_TEMPLATE_SUCCESS,
+ CREATE_PROJECT_TYPE_SUCCESS,
+ REMOVE_PRODUCT_CATEGORY_PENDING,
+ REMOVE_PROJECT_TYPE_PENDING,
+ REMOVE_PRODUCT_CATEGORY_FAILURE,
+ REMOVE_PROJECT_TYPE_FAILURE,
+ REMOVE_PRODUCT_CATEGORY_SUCCESS,
+ REMOVE_PROJECT_TYPE_SUCCESS,
+ PRODUCT_CATEGORIES_SORT,
+ REMOVE_PROJECT_TEMPLATE_SUCCESS,
+ REMOVE_PRODUCT_TEMPLATE_SUCCESS,
+ REMOVE_PROJECT_TEMPLATE_FAILURE,
+ REMOVE_PRODUCT_TEMPLATE_FAILURE,
+ REMOVE_PROJECT_TEMPLATE_PENDING,
+ REMOVE_PRODUCT_TEMPLATE_PENDING
} from '../config/constants'
+import Alert from 'react-s-alert'
export const initialState = {
projectTemplates: null,
projectTypes: null,
productTemplates: null,
productCategories: null,
+ milestoneTemplates: null,
isLoading: false,
+ isRemoving: false,
+ error: false,
}
export default function(state = initialState, action) {
@@ -16,17 +57,147 @@ export default function(state = initialState, action) {
case LOAD_PROJECTS_METADATA_PENDING:
return {
...state,
- isLoading: true,
+ isLoading: true
}
case LOAD_PROJECTS_METADATA_SUCCESS: {
- const { projectTemplates, projectTypes, productTemplates, productCategories } = action.payload
+ const { projectTemplates, projectTypes, productTemplates, productCategories, milestoneTemplates } = action.payload
+ return {
+ ...state,
+ projectTemplates: _.orderBy(projectTemplates, ['updatedAt'], ['desc']),
+ projectTypes: _.orderBy(projectTypes, ['updatedAt'], ['desc']),
+ productTemplates: _.orderBy(productTemplates, ['updatedAt'], ['desc']),
+ productCategories: _.orderBy(productCategories, ['updatedAt'], ['desc']),
+ milestoneTemplates,
+ isLoading: false,
+ }
+ }
+ case ADD_PROJECTS_METADATA_PENDING:
+ case CREATE_PROJECT_TEMPLATE_PENDING:
+ case CREATE_PRODUCT_TEMPLATE_PENDING:
+ case CREATE_PROJECT_TYPE_PENDING:
+ case UPDATE_PROJECTS_METADATA_PENDING:
+ return {
+ ...state,
+ isLoading: true
+ }
+ case REMOVE_PROJECTS_METADATA_PENDING:
+ case REMOVE_PRODUCT_CATEGORY_PENDING:
+ case REMOVE_PROJECT_TYPE_PENDING:
+ case REMOVE_PROJECT_TEMPLATE_PENDING:
+ case REMOVE_PRODUCT_TEMPLATE_PENDING:
+ return {
+ ...state,
+ isRemoving: true
+ }
+ case ADD_PROJECTS_METADATA_FAILURE:
+ case CREATE_PROJECT_TEMPLATE_FAILURE:
+ case CREATE_PRODUCT_TEMPLATE_FAILURE:
+ case CREATE_PROJECT_TYPE_FAILURE:
+ Alert.error(`PROJECT METADATA CREATE FAILED: ${action.payload.response.data.result.content.message}`)
+ return {
+ ...state,
+ isLoading: false,
+ error: action.payload.response.data.result.content.message
+ }
+ case UPDATE_PROJECTS_METADATA_FAILURE:
+ Alert.error(`PROJECT METADATA UPDATE FAILED: ${action.payload.response.data.result.content.message}`)
+ return {
+ ...state,
+ isLoading: false,
+ error: action.payload.response.data.result.content.message
+ }
+ case REMOVE_PROJECTS_METADATA_FAILURE:
+ case REMOVE_PRODUCT_CATEGORY_FAILURE:
+ case REMOVE_PROJECT_TYPE_FAILURE:
+ case REMOVE_PROJECT_TEMPLATE_FAILURE:
+ case REMOVE_PRODUCT_TEMPLATE_FAILURE:
+ Alert.error(`PROJECT METADATA DELETE FAILED: ${action.payload.response.data.result.content.message}`)
+ return {
+ ...state,
+ isRemoving: false,
+ error: action.payload.response.data.result.content.message
+ }
+ case ADD_PROJECTS_METADATA_SUCCESS:
+ case CREATE_PROJECT_TEMPLATE_SUCCESS:
+ case CREATE_PRODUCT_TEMPLATE_SUCCESS:
+ case CREATE_PROJECT_TYPE_SUCCESS:
+ Alert.success('PROJECT METADATA CREATE SUCCESS')
return {
...state,
- projectTemplates,
- projectTypes,
- productTemplates,
- productCategories,
isLoading: false,
+ metadata: action.payload,
+ error: false,
+ }
+ case UPDATE_PROJECTS_METADATA_SUCCESS:
+ Alert.success('PROJECT METADATA UPDATE SUCCESS')
+ return {
+ ...state,
+ isLoading: false,
+ metadata: action.payload,
+ error: false,
+ }
+ case REMOVE_PROJECTS_METADATA_SUCCESS:
+ case REMOVE_PRODUCT_CATEGORY_SUCCESS:
+ case REMOVE_PROJECT_TYPE_SUCCESS:
+ case REMOVE_PROJECT_TEMPLATE_SUCCESS: {
+ Alert.success('PROJECT METADATA DELETE SUCCESS')
+ // TODO remove metadata from the state
+ let projectTemplates = state.projectTemplates
+ const metadataId = action.payload.metadataId
+ if (action.payload.type === 'projectTemplates') {
+ if (metadataId) {
+ projectTemplates = _.filter(projectTemplates, m => m.id !== metadataId)
+ }
+ }
+ return update (state, {
+ isRemoving: { $set : false },
+ error: { $set : false },
+ projectTemplates: { $set : projectTemplates }
+ })
+ }
+ case REMOVE_PRODUCT_TEMPLATE_SUCCESS: {
+ Alert.success('PRODUCT DELETE SUCCESS')
+ let productTemplates = state.productTemplates
+ const metadataId = action.payload.metadataId
+ if (metadataId) {
+ productTemplates = _.filter(productTemplates, m => m.id !== metadataId)
+ }
+ return update (state, {
+ isRemoving: { $set : false },
+ error: { $set : false },
+ productTemplates: { $set : productTemplates }
+ })
+ }
+ case PROJECT_TEMPLATES_SORT: {
+ const fieldName = action.payload.fieldName
+ const order = action.payload.order
+ return {
+ ...state,
+ projectTemplates: _.orderBy(state.projectTemplates, [`${fieldName}`], [`${order}`]),
+ }
+ }
+ case PRODUCT_TEMPLATES_SORT: {
+ const fieldName = action.payload.fieldName
+ const order = action.payload.order
+ return {
+ ...state,
+ productTemplates: _.orderBy(state.productTemplates, [`${fieldName}`], [`${order}`]),
+ }
+ }
+ case PROJECT_TYPES_SORT: {
+ const fieldName = action.payload.fieldName
+ const order = action.payload.order
+ return {
+ ...state,
+ projectTypes: _.orderBy(state.projectTypes, [`${fieldName}`], [`${order}`]),
+ }
+ }
+ case PRODUCT_CATEGORIES_SORT: {
+ const fieldName = action.payload.fieldName
+ const order = action.payload.order
+ return {
+ ...state,
+ productCategories: _.orderBy(state.productCategories, [`${fieldName}`], [`${order}`]),
}
}
default: return state
diff --git a/src/routes.jsx b/src/routes.jsx
index a12afb206..b4467f09b 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -8,6 +8,7 @@ import CoderBot from './components/CoderBot/CoderBot'
import projectRoutes from './projects/routes.jsx'
import notificationsRoutes from './routes/notifications/routes.jsx'
import settingsRoutes from './routes/settings/routes.jsx'
+import metaDataRoutes from './routes/metadata/routes.jsx'
import TopBarContainer from './components/TopBar/TopBarContainer'
import ProjectsToolBar from './components/TopBar/ProjectsToolBar'
import RedirectComponent from './components/RedirectComponent'
@@ -148,6 +149,7 @@ class Routes extends React.Component {
{/* {reportsListRoutes} */}
{notificationsRoutes}
{settingsRoutes}
+ {metaDataRoutes}
)} />
)} />
diff --git a/src/routes/metadata/components/FullScreenJSONEditor.jsx b/src/routes/metadata/components/FullScreenJSONEditor.jsx
new file mode 100644
index 000000000..ca6e4cf84
--- /dev/null
+++ b/src/routes/metadata/components/FullScreenJSONEditor.jsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import JSONInput from 'react-json-editor-ajrm'
+import locale from 'react-json-editor-ajrm/locale/en'
+
+import MobilePage from '../../../components/MobilePage/MobilePage'
+import './FullScreenJSONEditor.scss'
+
+const FullScreenJSONEditor = ({
+ onJSONEdit,
+ onExit,
+ json
+}) => (
+
+
+ Restore
+
+
+
+)
+
+export default FullScreenJSONEditor
diff --git a/src/routes/metadata/components/FullScreenJSONEditor.scss b/src/routes/metadata/components/FullScreenJSONEditor.scss
new file mode 100644
index 000000000..ac2d1a5b4
--- /dev/null
+++ b/src/routes/metadata/components/FullScreenJSONEditor.scss
@@ -0,0 +1,8 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+.full-screen-json-editor {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ background-color: $tc-gray-neutral-light;
+}
\ No newline at end of file
diff --git a/src/routes/metadata/components/MetaDataLayout.jsx b/src/routes/metadata/components/MetaDataLayout.jsx
new file mode 100644
index 000000000..ef9803a64
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataLayout.jsx
@@ -0,0 +1,26 @@
+import React from 'react'
+import SecondaryToolBarContainer from '../containers/SecondaryToolBarContainer'
+import './MetaDataLayout.scss'
+
+class MetaDataLayout extends React.Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+
+ const location = this.props.location.pathname
+ return (
+
+
+ { /* pass location, to make sure that component is re-rendered when location is changed
+ it's necessary to update the active state of the tabs */ }
+
+
+ {this.props.main}
+
+ )
+ }
+}
+
+export default MetaDataLayout
diff --git a/src/routes/metadata/components/MetaDataLayout.scss b/src/routes/metadata/components/MetaDataLayout.scss
new file mode 100644
index 000000000..fd7fd456d
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataLayout.scss
@@ -0,0 +1,23 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+.container {
+
+ @media screen and (min-width: $screen-md) {
+ padding-top: 50px;
+ }
+}
+
+.secondary-toolbar {
+ box-shadow: 0 4px 3px -3px $tc-gray-20, 0 1px 0 0 $tc-gray-20, 0 -1px 0 0 $tc-gray-05;
+ position: relative;
+ z-index: 5;
+
+ @media screen and (min-width: $screen-md) {
+ position: fixed;
+ top: 60px;
+ left: 0;
+ right: 0;
+ width: 100%;
+ }
+}
+
diff --git a/src/routes/metadata/components/MetaDataPanel.jsx b/src/routes/metadata/components/MetaDataPanel.jsx
new file mode 100644
index 000000000..989f5804c
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataPanel.jsx
@@ -0,0 +1,636 @@
+/**
+ * Panel component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import JSONInput from 'react-json-editor-ajrm'
+import locale from 'react-json-editor-ajrm/locale/en'
+import _ from 'lodash'
+import update from 'react-addons-update'
+import Sticky from '../../../components/Sticky'
+import MediaQuery from 'react-responsive'
+// import ReactJson from 'react-json-view'
+import SpecSection from '../../../projects/detail/components/SpecSection'
+import TemplateForm from './TemplateForm'
+import CoderBroken from '../../../assets/icons/coder-broken.svg'
+import { SCREEN_BREAKPOINT_MD } from '../../../config/constants'
+
+import './MetaDataPanel.scss'
+import FullScreenJSONEditor from './FullScreenJSONEditor'
+
+const phasesDefaultValue = {
+ '1-dev-iteration-i': {
+ name: 'Dev Iteration',
+ products: [
+ {
+ productKey: 'development-iteration-5-milestones',
+ id: 29
+ }
+ ],
+ duration: 25
+ }
+}
+const sectionsDefaultValue = [
+ {
+ id: 'appDefinition',
+ title: 'Sample Project',
+ required: true,
+ description: 'Please answer a few basic questions about your project and, as an option, add links to supporting documents in the “Notes” section. If you have any files to upload, you’ll be able to do so later.',
+ subSections: [
+ {
+ id: 'projectName',
+ required: true,
+ validationError: 'Please provide a name for your project',
+ fieldName: 'name',
+ description: '',
+ title: 'Project Name',
+ type: 'project-name'
+ },
+ {
+ id: 'questions',
+ required: true,
+ hideTitle: true,
+ title: 'Questions',
+ description: '',
+ type: 'questions',
+ questions: [
+ {
+ id: 'projectInfo',
+ validations: 'isRequired,minLength:160',
+ validationErrors: {
+ isRequired: 'Please provide a description',
+ minLength: 'Please enter at least 160 characters'
+ },
+ fieldName: 'description',
+ description: 'Brief Description',
+ title: 'Description',
+ type: 'textbox'
+ },
+ {
+ icon: 'question',
+ required: true,
+ validationError: 'Please let us know consumers of your application',
+ title: 'What type of question you want to see next?',
+ description: 'Description for the radio button type question',
+ type: 'radio-group',
+ fieldName: 'details.appDefinition.questionType',
+ options: [
+ {
+ value: 'checkbox-group',
+ label: 'Checkbox Group'
+ },
+ {
+ value: 'slide-radiogroup',
+ label: 'Slide Radio Group'
+ },
+ {
+ value: 'tiled-radio-group',
+ label: 'Tiled Radio Group'
+ }
+ ]
+ },
+ {
+ icon: 'question',
+ required: true,
+ validationError: 'Validation error for tiled radio group question',
+ title: 'Sample tiled radio group question?',
+ description: 'Description for tiled radio group question',
+ fieldName: 'details.appDefinition.sampleTiledRadioGroup',
+ type: 'tiled-radio-group',
+ condition: 'details.appDefinition.questionType == \'tiled-radio-group\'',
+ options: [
+ {
+ value: 'value1',
+ title: 'Value 1',
+ icon: 'icon-test-unstructured',
+ iconOptions: {
+ fill: '#00000'
+ },
+ desc: ''
+ },
+ {
+ value: 'value2',
+ title: 'Value 2',
+ icon: 'icon-test-structured',
+ iconOptions: {
+ fill: '#00000'
+ },
+ desc: ''
+ },
+ {
+ value: 'value3',
+ title: 'Value 3',
+ icon: 'icon-dont-know',
+ iconOptions: {
+ fill: '#00000'
+ },
+ desc: ''
+ }
+ ]
+ },
+ {
+ icon: 'question',
+ title: 'Sample Checkbox group type question',
+ description: 'Description for checkbox group type question',
+ fieldName: 'details.appDefinition.sampleCheckboxGroup',
+ type: 'checkbox-group',
+ condition: 'details.appDefinition.questionType == \'checkbox-group\'',
+ options: [
+ {
+ value: 'value1',
+ label: 'Value 1'
+ },
+ {
+ value: 'value2',
+ label: 'Value 2'
+ }
+ ]
+ },
+ {
+ icon: 'question',
+ description: 'How much budget do you have?',
+ title: 'Sample Slide Radio Group type question',
+ fieldName: 'details.appDefinition.sampleSlideRadioGroup',
+ type: 'slide-radiogroup',
+ condition: 'details.appDefinition.questionType == \'slide-radiogroup\'',
+ options: [
+ {
+ value: 'upto-25',
+ title: 'Under $25K '
+ },
+ {
+ value: 'upto-50',
+ title: '$25K to $50K'
+ },
+ {
+ value: 'upto-75',
+ title: '$50K to $75K'
+ },
+ {
+ value: 'upto-100',
+ title: '$75K to $100K'
+ },
+ {
+ value: 'above-100',
+ title: 'More than $100K'
+ }
+ ],
+ required: true,
+ validationError: 'Please provide value for sample radio group question'
+ }
+ ]
+ },
+ {
+ id: 'notes',
+ fieldName: 'details.appDefinition.notes',
+ title: 'Notes',
+ description: 'Add any other important information regarding your project (e.g. links to documents or existing applications)',
+ type: 'notes'
+ }
+ ]
+ }
+]
+
+class MetaDataPanel extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ currentTemplateType: '',
+ metadataType: '',
+ currentTemplateName: '',
+ primaryKeyName: '',
+ metadata: null,
+ isNew: false,
+ isProcessing: false,
+ project: {},
+ fields: [],
+ isFullScreen: false
+ }
+ this.init = this.init.bind(this)
+ this.getMetadata = this.getMetadata.bind(this)
+ this.getProjectTypeOptions = this.getProjectTypeOptions.bind(this)
+ this.getProductCategoryOptions = this.getProductCategoryOptions.bind(this)
+ this.getResourceNameFromType = this.getResourceNameFromType.bind(this)
+ this.renderSection = this.renderSection.bind(this)
+ this.enterFullScreen = this.enterFullScreen.bind(this)
+ this.exitFullScreen = this.exitFullScreen.bind(this)
+ this.onJSONEdit = this.onJSONEdit.bind(this)
+ this.onCreate = this.onCreate.bind(this)
+
+ this.onCreateTemplate = this.onCreateTemplate.bind(this)
+ this.onSaveTemplate = this.onSaveTemplate.bind(this)
+ this.onDeleteTemplate = this.onDeleteTemplate.bind(this)
+ this.onChangeTemplate = this.onChangeTemplate.bind(this)
+ }
+
+ componentDidMount() {
+ document.title = 'Metadata Management - TopCoder'
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.init(nextProps)
+ }
+
+ componentWillMount() {
+ const { templates } = this.props
+ if (templates && (!templates.productTemplates && !templates.isLoading)) {
+ this.props.loadProjectsMetadata()
+ } else {
+ this.init(this.props)
+ }
+ }
+
+ init(props) {
+ const { metadataType, isNew, templates } = props
+ this.setState({
+ project: {
+ details: {appDefinition: {}}, version: 'v2'
+ },
+ dirtyProject: {details: {}, version: 'v2'},
+ fields: this.getFields(props),
+ metadata: this.getMetadata(props),
+ metadataType,
+ isNew,
+ isUpdating: templates.isLoading,
+ })
+ }
+
+ getMetadata(props) {
+ const { metadata, metadataType, isNew } = props
+ const { metadata : dirtyMetadata } = this.state
+ if (isNew && !metadata && !dirtyMetadata) {
+ if (metadataType === 'projectTemplate') {
+ return { scope: { sections: sectionsDefaultValue } }
+ }
+ if (metadataType === 'productTemplate') {
+ return { template: { questions: sectionsDefaultValue } }
+ }
+ return {}
+ }
+ return metadata ? metadata : dirtyMetadata
+ }
+
+ getResourceNameFromType(type) {
+ if (type === 'productCategory') {
+ return 'productCategories'
+ }
+ return type + 's'
+ }
+
+ getProductCategoryOptions(productCategories) {
+ return _.map(productCategories, (category) => {
+ return {
+ value: category.key,
+ title: category.displayName
+ }
+ })
+
+ }
+
+ getProjectTypeOptions(projectTypes) {
+ return _.map(projectTypes, (type) => {
+ return {
+ value: type.key,
+ title: type.displayName
+ }
+ })
+ }
+
+ /**
+ * get all fields of metadata
+ */
+ getFields(props) {
+ const { metadataType, templates } = props
+ let fields = []
+ const metadata = this.getMetadata(props)
+ if (metadataType === 'productTemplate') {
+ const prodCatOptions = this.getProductCategoryOptions(templates.productCategories)
+ const categoryValue = metadata.category ? metadata.category : prodCatOptions[0].value
+ const subCategoryValue = metadata.subCategory ? metadata.subCategory : prodCatOptions[0].value
+ fields = fields.concat([
+ { key: 'id', type: 'number' },
+ { key: 'name', type: 'text' },
+ { key: 'productKey', type: 'text' },
+ { key: 'category', type: 'dropdown', options: prodCatOptions, value: categoryValue },
+ { key: 'subCategory', type: 'dropdown', options: prodCatOptions, value: subCategoryValue },
+ { key: 'icon', type: 'text' },
+ { key: 'brief', type: 'text' },
+ { key: 'details', type: 'text' },
+ { key: 'aliases', type: 'array' },
+ { key: 'disabled', type: 'checkbox' },
+ { key: 'hidden', type: 'checkbox' },
+ ])
+ } else if (metadataType === 'projectTemplate') {
+ const projectTypeOptions = this.getProductCategoryOptions(templates.projectTypes)
+ const value = metadata.category ? metadata.category : projectTypeOptions[0].value
+ fields = fields.concat([
+ { key: 'id', type: 'number' },
+ { key: 'name', type: 'text' },
+ { key: 'key', type: 'text' },
+ { key: 'category', type: 'dropdown', options: projectTypeOptions, value },
+ { key: 'icon', type: 'text' },
+ { key: 'question', type: 'text' },
+ { key: 'info', type: 'text' },
+ { key: 'aliases', type: 'array' },
+ { key: 'phases', type: 'json', value: phasesDefaultValue },
+ { key: 'disabled', type: 'checkbox' },
+ { key: 'hidden', type: 'checkbox' },
+ ])
+ } else if (metadataType === 'projectType') {
+ fields = fields.concat([
+ { key: 'key', type: 'text' },
+ { key: 'displayName', type: 'text' },
+ { key: 'icon', type: 'text' },
+ { key: 'question', type: 'text' },
+ { key: 'info', type: 'text' },
+ { key: 'aliases', type: 'array' },
+ { key: 'metadata', type: 'json' },
+ { key: 'disabled', type: 'checkbox' },
+ { key: 'hidden', type: 'checkbox' },
+ ])
+ } else if (metadataType === 'productCategory') {
+ fields = fields.concat([
+ { key: 'key', type: 'text' },
+ { key: 'displayName', type: 'text' },
+ { key: 'icon', type: 'text' },
+ { key: 'question', type: 'text' },
+ { key: 'info', type: 'text' },
+ { key: 'aliases', type: 'array' },
+ // { key: 'metadata', type: 'json' },
+ { key: 'disabled', type: 'checkbox' },
+ { key: 'hidden', type: 'checkbox' },
+ ])
+ }
+ return fields
+ }
+
+ onCreate() {
+ this.onCreateTemplate(false)
+ }
+
+ /**
+ * create new template
+ */
+ onCreateTemplate(isDuplicate) {
+ const { fields, metadata, metadataType } = this.state
+ const newValues = _.assign({}, metadata)
+ if (!isDuplicate) {
+ _.forEach(fields, (field) => {
+ switch (field.type) {
+ case 'checkbox':
+ newValues[field.key] = false
+ break
+ default:
+ newValues[field.key] = null
+ break
+ }
+ })
+ if (metadataType === 'productTemplate') {
+ newValues.template = {}
+ }
+
+ if (metadataType === 'projectTemplate') {
+ newValues.scope = {}
+ }
+ } else {
+ if (newValues.hasOwnProperty('id')) {
+ newValues.id = null
+ } else {
+ newValues.key = null
+ }
+ }
+
+ this.setState({
+ metadata: newValues,
+ isNew: true,
+ })
+ }
+
+ /**
+ * save template
+ */
+ onSaveTemplate(id, data) {
+ const {metadataType, isNew } = this.state
+ const omitKeys = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']
+ if (!isNew) {
+ if (metadataType === 'productTemplates') {
+ omitKeys.push('aliases')
+ }
+ const payload = _.omit(data, omitKeys)
+ const metadataResource = this.getResourceNameFromType(metadataType)
+ this.props.updateProjectsMetadata(id, metadataResource, payload)
+ .then((res) => {
+ if (!res.error) {
+ this.props.loadProjectsMetadata()
+ }
+ })
+ } else {
+ const payload = _.omit(data, omitKeys)
+ const metadataResource = this.getResourceNameFromType(metadataType)
+ this.props.createProjectsMetadata(payload)
+ .then((res) => {
+ if (!res.error) {
+ const createdMetadata = res.action.payload
+ if (['projectTemplate', 'productTemplate'].indexOf(metadataType) !== -1) {
+ window.location = `/metadata/${metadataResource}/${createdMetadata.id}`
+ } else {
+ window.location = `/metadata/${metadataResource}/${createdMetadata.key}`
+ }
+ }
+ })
+ }
+ }
+
+ /**
+ * delete template
+ */
+ onDeleteTemplate(value) {
+ const {metadataType} = this.state
+ const metadataResource = this.getResourceNameFromType(metadataType)
+ this.props.deleteProjectsMetadata(value, metadataResource)
+ .then((res) => {
+ if (!res.error) {
+ window.location = `/metadata/${metadataResource}`
+ }
+ })
+ }
+
+ /**
+ * change current template
+ */
+ onChangeTemplate(data) {
+ const { metadata } = this.state
+ const newTemplate = _.assign({}, metadata, data)
+ this.setState({
+ metadata: newTemplate,
+ })
+ }
+
+ onJSONEdit({ jsObject }) {
+ const { metadataType } = this.state
+ if (metadataType === 'productTemplate') {
+ const updateQuery = { template : { $set : jsObject } }
+ this.setState(update(this.state, { metadata: updateQuery }))
+ }
+ if (metadataType === 'projectTemplate') {
+ const updateQuery = { scope : { $set : jsObject } }
+ this.setState(update(this.state, { metadata: updateQuery }))
+ }
+ }
+
+ enterFullScreen() {
+ this.setState({ isFullScreen : true })
+ }
+
+ exitFullScreen() {
+ this.setState({ isFullScreen : false })
+ }
+
+ renderSection(section, idx) {
+ return (
+
+
{} }
+ showFeaturesDialog={() => {} }
+ // TODO we shoudl not update the props (section is coming from props)
+ validate={(isInvalid) => section.isInvalid = isInvalid}
+ showHidden={false}
+ addAttachment={ () => {} }
+ updateAttachment={ () => {} }
+ removeAttachment={ () => {} }
+ attachmentsStorePath={'dummy'}
+ canManageAttachments
+ />
+
+ Save Changes
+
+
+ )
+ }
+
+ render() {
+ const { isAdmin, metadataType, templates } = this.props
+ const { fields, metadata, isNew, isFullScreen } = this.state
+ let template = {}
+ let templateSections = []
+ let needTemplatePreview = false
+ if (metadata && metadataType === 'projectTemplate' && metadata.scope) {
+ template = metadata.scope
+ templateSections = template.sections
+ needTemplatePreview = true
+ } else if (metadata && metadataType === 'productTemplate' && metadata.template) {
+ template = metadata.template
+ templateSections = template.questions
+ needTemplatePreview = true
+ }
+ // console.log(templates)
+
+ if (!isAdmin) {
+ return (
+
+
+
+
+ You don't have permission to access Metadata Management
+
+
+
+ )
+ }
+
+ return (
+
+ {
+ isFullScreen && (
+
+ )
+ }
+ { needTemplatePreview &&
+
+ {
+ //render preview for intake form
+ templateSections && (
+
+
+
Template form preview
+
+
+
+ {templateSections.map(this.renderSection)}
+
+
+
+
+
+ )
+ }
+
+ }
+
+ { (metadata || isNew) && (['projectTemplate', 'productTemplate'].indexOf(metadataType) !== -1) && (
+
+ Maximize
+
+ {/* */}
+
+ )
+ }
+ { !templates.isLoading && (!!metadata || isNew ) && (
+ )
+ }
+
+
+ )
+ }
+}
+
+MetaDataPanel.propTypes = {
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProjectsMetadata: PropTypes.func.isRequired,
+ createProjectsMetadata: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+ templates: PropTypes.object.isRequired,
+ isAdmin: PropTypes.bool.isRequired,
+}
+
+export default MetaDataPanel
diff --git a/src/routes/metadata/components/MetaDataPanel.scss b/src/routes/metadata/components/MetaDataPanel.scss
new file mode 100644
index 000000000..927e152fc
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataPanel.scss
@@ -0,0 +1,106 @@
+// this is to include tc styles in the output library
+@import '~tc-ui/src/styles/tc-includes';
+
+:global {
+ .meta-data-panel {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 auto;
+ min-width: 960px;
+ padding: 30px 20px 0;
+
+ > .content {
+ flex: 1;
+ margin-right: 20px;
+
+ .ProjectWizard {
+ padding: 10px;
+
+ .section-footer {
+ margin-top: 0px;// resets the negative margin which is used in actual rendering
+ }
+ }
+ }
+
+ > .filters {
+ flex: 1;
+ background-color: $tc-white;
+ padding: 10px;
+ border-radius: 6px;
+ max-width: 340px;
+
+ .json_editor_wrapper {
+ overflow: scroll;
+ display: flex;
+ flex-direction: column;
+
+ .maximize-btn {
+ align-self: flex-end;
+ }
+ }
+ }
+
+ h5 {
+ margin-left: 10px;
+ }
+
+ .align-left {
+ margin-left: 10px;
+ margin-bottom: 20px;
+ }
+
+ .input-field {
+ :global(.Dropdown) {
+ overflow-y: scroll;
+ height: 500px;
+ }
+
+ .clear-margin {
+ :global(.dropdown-wrap) {
+ margin-left: 0;
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+
+ .content.content-error {
+ .container {
+ width: 100%;
+ background: $tc-gray-neutral-dark;
+ }
+ }
+
+ .page-error {
+ border-radius:4px;
+ position: relative;
+ padding-top: 55px;
+ text-align: center;
+ min-height: 706px;
+
+ margin: 20px auto 0;
+ background: $tc-white;
+ .icon-coder-broken {
+ margin: 6% 20%;
+ }
+ background-size: 307px 300px;
+ h3{
+ color: $tc-gray-70;
+ @include roboto-medium;
+ font-size: 48px;
+ letter-spacing: 0px;
+ padding: 0 168px 25px 168px;
+ line-height: inherit;
+ }
+
+ span{
+ position: absolute;
+ left: 40%;
+ bottom: 55%;
+ @include roboto-medium;
+ font-size: 25px;
+ color: $tc-gray-70;
+ letter-spacing: 0px;
+ }
+ }
+}
diff --git a/src/routes/metadata/components/MetaDataProjectTemplatesGridView.scss b/src/routes/metadata/components/MetaDataProjectTemplatesGridView.scss
new file mode 100644
index 000000000..6cc199741
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataProjectTemplatesGridView.scss
@@ -0,0 +1,78 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+/* cards are arranged in one column when width is less than 720, as one card minimum width is 360 */
+$screen-one-column: 720px;
+
+:global {
+ .project-templates-grid-view {
+
+ .project-templates-actions {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ margin: 10px 20px;
+ }
+ .flex-data {
+ .flex-row {
+ .item-id {
+ flex: none;
+ min-width: 80px;
+ width: 80px;
+ }
+ .item-key {
+ flex: 6 1 160px;
+ min-width: 160px;
+ width: 160px;
+ padding-left: 20px;
+ padding-right: 20px;
+ }
+
+ .item-icon {
+ flex: none;
+ min-width: 52px;
+ width: 52px;
+ }
+
+ .item-project-templates {
+ flex: 6 1 505px;
+ min-width: 505px;
+ overflow: hidden;
+ }
+
+ .item-status-date {
+ flex: 1 3 142px;
+ min-width: 142px;
+ }
+
+ .item-disable-status,
+ .item-hidden-status {
+ flex: none;
+ min-width: 70px;
+ padding-right: 20px;
+ // width: 40px;
+ position: relative;
+
+ .modal-active {
+ position: absolute;
+ top: 0;
+ right: 100%;
+ width: 280px;
+ z-index: 10;
+
+ .status-header {
+ justify-content: flex-end;
+ position: relative;
+ top: 28px;
+ right: -30px;
+ }
+
+ .modal {
+ box-shadow: 0 0 4px 0 $tc-gray-20;
+ top: 25px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/routes/metadata/components/MetaDataToolBar.jsx b/src/routes/metadata/components/MetaDataToolBar.jsx
new file mode 100644
index 000000000..535bd691a
--- /dev/null
+++ b/src/routes/metadata/components/MetaDataToolBar.jsx
@@ -0,0 +1,11 @@
+/**
+ * MetaData pages tool bar
+ */
+import React from 'react'
+import SectionTopBar from '../../../components/TopBar/SectionToolBar'
+
+const MetaDataToolBar = () => (
+
+)
+
+export default MetaDataToolBar
diff --git a/src/routes/metadata/components/ProductCategoriesGridView.jsx b/src/routes/metadata/components/ProductCategoriesGridView.jsx
new file mode 100644
index 000000000..87d71623a
--- /dev/null
+++ b/src/routes/metadata/components/ProductCategoriesGridView.jsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import _ from 'lodash'
+import { NavLink, Link } from 'react-router-dom'
+import moment from 'moment'
+import GridView from '../../../components/Grid/GridView'
+
+import './MetaDataProjectTemplatesGridView.scss'
+
+const ProductCategoriesGridView = props => {
+ const { totalCount, criteria, pageNum, pageSize, sortHandler,
+ error, isLoading, infiniteAutoload, setInfiniteAutoload,
+ applyFilters, productCategories } = props
+
+ const currentSortField = _.get(criteria, 'sort', '')
+ // This 'little' array is the heart of the list component.
+ // it defines what columns should be displayed and more importantly
+ // how they should be displayed.
+ const columns = [
+ {
+ id: 'key',
+ headerLabel: 'Key',
+ classes: 'item-key',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/productCategories/${item.key}`
+ const recentlyCreated = moment().diff(item.createdAt, 'seconds') < 3600
+ return (
+
+ {recentlyCreated && }
+ {item.key}
+
+ )
+ }
+ }, {
+ id: 'displayName',
+ headerLabel: 'Display Name',
+ classes: 'item-project-templates',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/productCategories/${item.key}`
+ return (
+
+
+ {_.unescape(item.displayName)}
+
+
+ )
+ }
+ }, {
+ id: 'updatedAt',
+ headerLabel: 'Updated At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.updatedAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'createdAt',
+ headerLabel: 'Created At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.createdAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'status',
+ headerLabel: 'Status',
+ sortable: false,
+ classes: 'item-disable-status',
+ renderText: item => {
+ return (
+
+ { item.disabled ? 'Disabled' : 'Active' }
+
+ )
+ }
+ }, {
+ id: 'hidden',
+ headerLabel: 'Hidden',
+ sortable: false,
+ classes: 'item-hidden-status',
+ renderText: item => {
+ return (
+
+ { item.hidden ? 'Hidden' : 'Visible' }
+
+ )
+ }
+ }
+ ]
+
+ const gridProps = {
+ error,
+ isLoading,
+ columns,
+ onPageChange: () => {}, // dummy, as we are not expecting paging yet in metadata views
+ sortHandler,
+ currentSortField,
+ resultSet: productCategories,
+ totalCount,
+ currentPageNum: pageNum,
+ pageSize,
+ infiniteAutoload,
+ infiniteScroll: true,
+ setInfiniteAutoload,
+ applyFilters,
+ entityName: 'product category',
+ entityNamePlural: 'product categories'
+ }
+
+ return (
+
+ )
+}
+
+
+ProductCategoriesGridView.propTypes = {
+ currentUser: PropTypes.object.isRequired,
+ totalCount: PropTypes.number.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ error: PropTypes.bool.isRequired,
+ // onPageChange: PropTypes.func.isRequired,
+ sortHandler: PropTypes.func.isRequired,
+ pageNum: PropTypes.number.isRequired,
+ criteria: PropTypes.object.isRequired,
+ productCategories: PropTypes.array.isRequired,
+}
+
+export default ProductCategoriesGridView
diff --git a/src/routes/metadata/components/ProductTemplatesGridView.jsx b/src/routes/metadata/components/ProductTemplatesGridView.jsx
new file mode 100644
index 000000000..c612b3299
--- /dev/null
+++ b/src/routes/metadata/components/ProductTemplatesGridView.jsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import _ from 'lodash'
+import { NavLink, Link } from 'react-router-dom'
+import moment from 'moment'
+import GridView from '../../../components/Grid/GridView'
+
+import './MetaDataProjectTemplatesGridView.scss'
+
+const ProductTemplatesGridView = props => {
+ const { totalCount, criteria, pageNum, pageSize, sortHandler,
+ error, isLoading, infiniteAutoload, setInfiniteAutoload,
+ applyFilters, productTemplates } = props
+
+ const currentSortField = _.get(criteria, 'sort', '')
+ // This 'little' array is the heart of the list component.
+ // it defines what columns should be displayed and more importantly
+ // how they should be displayed.
+ const columns = [
+ {
+ id: 'id',
+ headerLabel: 'ID',
+ classes: 'item-id',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/productTemplates/${item.id}`
+ const recentlyCreated = moment().diff(item.createdAt, 'seconds') < 3600
+ return (
+
+ {recentlyCreated && }
+ {item.id}
+
+ )
+ }
+ }, {
+ id: 'templateName',
+ headerLabel: 'Template',
+ classes: 'item-project-templates',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/productTemplates/${item.id}`
+ return (
+
+
+ {_.unescape(item.name)}
+
+
+ )
+ }
+ }, {
+ id: 'updatedAt',
+ headerLabel: 'Updated At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.updatedAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'createdAt',
+ headerLabel: 'Created At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.createdAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'status',
+ headerLabel: 'Status',
+ sortable: false,
+ classes: 'item-disable-status',
+ renderText: item => {
+ return (
+
+ { item.disabled ? 'Disabled' : 'Active' }
+
+ )
+ }
+ }, {
+ id: 'hidden',
+ headerLabel: 'Hidden',
+ sortable: false,
+ classes: 'item-hidden-status',
+ renderText: item => {
+ return (
+
+ { item.hidden ? 'Hidden' : 'Visible' }
+
+ )
+ }
+ }
+ ]
+
+ const gridProps = {
+ error,
+ isLoading,
+ columns,
+ onPageChange: () => {}, // dummy, as we are not expecting paging yet in metadata views
+ sortHandler,
+ currentSortField,
+ resultSet: productTemplates,
+ totalCount,
+ currentPageNum: pageNum,
+ pageSize,
+ infiniteAutoload,
+ infiniteScroll: true,
+ setInfiniteAutoload,
+ applyFilters,
+ entityName: 'product template',
+ entityNamePlural: 'product templates'
+ }
+
+ return (
+
+ )
+}
+
+
+ProductTemplatesGridView.propTypes = {
+ currentUser: PropTypes.object.isRequired,
+ totalCount: PropTypes.number.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ error: PropTypes.bool.isRequired,
+ // onPageChange: PropTypes.func.isRequired,
+ sortHandler: PropTypes.func.isRequired,
+ pageNum: PropTypes.number.isRequired,
+ criteria: PropTypes.object.isRequired,
+ productTemplates: PropTypes.array.isRequired,
+}
+
+export default ProductTemplatesGridView
diff --git a/src/routes/metadata/components/ProjectTemplatesGridView.jsx b/src/routes/metadata/components/ProjectTemplatesGridView.jsx
new file mode 100644
index 000000000..9564eb0b3
--- /dev/null
+++ b/src/routes/metadata/components/ProjectTemplatesGridView.jsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import _ from 'lodash'
+import { NavLink, Link } from 'react-router-dom'
+import moment from 'moment'
+import GridView from '../../../components/Grid/GridView'
+
+import './MetaDataProjectTemplatesGridView.scss'
+
+const ProjectTemplatesGridView = props => {
+ const { totalCount, criteria, pageNum, pageSize, sortHandler,
+ error, isLoading, infiniteAutoload, setInfiniteAutoload,
+ applyFilters, projectTemplates } = props
+
+ const currentSortField = _.get(criteria, 'sort', '')
+ // This 'little' array is the heart of the list component.
+ // it defines what columns should be displayed and more importantly
+ // how they should be displayed.
+ const columns = [
+ {
+ id: 'id',
+ headerLabel: 'ID',
+ classes: 'item-id',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/projectTemplates/${item.id}`
+ const recentlyCreated = moment().diff(item.createdAt, 'seconds') < 3600
+ return (
+
+ {recentlyCreated && }
+ {item.id}
+
+ )
+ }
+ }, {
+ id: 'templateName',
+ headerLabel: 'Template',
+ classes: 'item-project-templates',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/projectTemplates/${item.id}`
+ return (
+
+
+ {_.unescape(item.name)}
+
+
+ )
+ }
+ }, {
+ id: 'updatedAt',
+ headerLabel: 'Updated At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.updatedAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'createdAt',
+ headerLabel: 'Created At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.createdAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'status',
+ headerLabel: 'Status',
+ sortable: false,
+ classes: 'item-disable-status',
+ renderText: item => {
+ return (
+
+ { item.disabled ? 'Disabled' : 'Active' }
+
+ )
+ }
+ }, {
+ id: 'hidden',
+ headerLabel: 'Hidden',
+ sortable: false,
+ classes: 'item-hidden-status',
+ renderText: item => {
+ return (
+
+ { item.hidden ? 'Hidden' : 'Visible' }
+
+ )
+ }
+ }
+ ]
+
+ const gridProps = {
+ error,
+ isLoading,
+ columns,
+ onPageChange: () => {}, // dummy, as we are not expecting paging yet in metadata views
+ sortHandler,
+ currentSortField,
+ resultSet: projectTemplates,
+ totalCount,
+ currentPageNum: pageNum,
+ pageSize,
+ infiniteAutoload,
+ infiniteScroll: true,
+ setInfiniteAutoload,
+ applyFilters,
+ entityName: 'project template',
+ entityNamePlural: 'project templates'
+ }
+
+ return (
+
+ )
+}
+
+
+ProjectTemplatesGridView.propTypes = {
+ currentUser: PropTypes.object.isRequired,
+ totalCount: PropTypes.number.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ error: PropTypes.bool.isRequired,
+ // onPageChange: PropTypes.func.isRequired,
+ sortHandler: PropTypes.func.isRequired,
+ pageNum: PropTypes.number.isRequired,
+ criteria: PropTypes.object.isRequired,
+ projectTemplates: PropTypes.array.isRequired,
+}
+
+export default ProjectTemplatesGridView
diff --git a/src/routes/metadata/components/ProjectTypesGridView.jsx b/src/routes/metadata/components/ProjectTypesGridView.jsx
new file mode 100644
index 000000000..9811e85b8
--- /dev/null
+++ b/src/routes/metadata/components/ProjectTypesGridView.jsx
@@ -0,0 +1,147 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import _ from 'lodash'
+import { NavLink, Link } from 'react-router-dom'
+import moment from 'moment'
+import GridView from '../../../components/Grid/GridView'
+
+import './MetaDataProjectTemplatesGridView.scss'
+
+const ProjectTypesGridView = props => {
+ const { totalCount, criteria, pageNum, pageSize, sortHandler,
+ error, isLoading, infiniteAutoload, setInfiniteAutoload,
+ applyFilters, projectTypes } = props
+
+ const currentSortField = _.get(criteria, 'sort', '')
+ // This 'little' array is the heart of the list component.
+ // it defines what columns should be displayed and more importantly
+ // how they should be displayed.
+ const columns = [
+ {
+ id: 'key',
+ headerLabel: 'Key',
+ classes: 'item-key',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/projectTypes/${item.key}`
+ const recentlyCreated = moment().diff(item.createdAt, 'seconds') < 3600
+ return (
+
+ {recentlyCreated && }
+ {item.key}
+
+ )
+ }
+ }, {
+ id: 'displayName',
+ headerLabel: 'Display Name',
+ classes: 'item-project-templates',
+ sortable: false,
+ renderText: item => {
+ const url = `/metadata/projectTypes/${item.key}`
+ return (
+
+
+ {_.unescape(item.displayName)}
+
+
+ )
+ }
+ }, {
+ id: 'updatedAt',
+ headerLabel: 'Updated At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.updatedAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'createdAt',
+ headerLabel: 'Created At',
+ sortable: true,
+ classes: 'item-status-date',
+ renderText: item => {
+ const time = moment(item.createdAt)
+ return (
+
+
{time.year() === moment().year() ? time.format('MMM D, h:mm a') : time.format('MMM D YYYY, h:mm a')}
+
+ )
+ }
+ }, {
+ id: 'status',
+ headerLabel: 'Status',
+ sortable: false,
+ classes: 'item-disable-status',
+ renderText: item => {
+ return (
+
+ { item.disabled ? 'Disabled' : 'Active' }
+
+ )
+ }
+ }, {
+ id: 'hidden',
+ headerLabel: 'Hidden',
+ sortable: false,
+ classes: 'item-hidden-status',
+ renderText: item => {
+ return (
+
+ { item.hidden ? 'Hidden' : 'Visible' }
+
+ )
+ }
+ }
+ ]
+
+ const gridProps = {
+ error,
+ isLoading,
+ columns,
+ onPageChange: () => {}, // dummy, as we are not expecting paging yet in metadata views
+ sortHandler,
+ currentSortField,
+ resultSet: projectTypes,
+ totalCount,
+ currentPageNum: pageNum,
+ pageSize,
+ infiniteAutoload,
+ infiniteScroll: true,
+ setInfiniteAutoload,
+ applyFilters,
+ entityName: 'project type',
+ entityNamePlural: 'project types'
+ }
+
+ return (
+
+ )
+}
+
+
+ProjectTypesGridView.propTypes = {
+ currentUser: PropTypes.object.isRequired,
+ totalCount: PropTypes.number.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ error: PropTypes.bool.isRequired,
+ // onPageChange: PropTypes.func.isRequired,
+ sortHandler: PropTypes.func.isRequired,
+ pageNum: PropTypes.number.isRequired,
+ criteria: PropTypes.object.isRequired,
+ projectTypes: PropTypes.array.isRequired,
+}
+
+export default ProjectTypesGridView
diff --git a/src/routes/metadata/components/TemplateForm.jsx b/src/routes/metadata/components/TemplateForm.jsx
new file mode 100644
index 000000000..cf6757214
--- /dev/null
+++ b/src/routes/metadata/components/TemplateForm.jsx
@@ -0,0 +1,403 @@
+/**
+ * Metadata Fields Form
+ */
+import _ from 'lodash'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Modal from 'react-modal'
+import JSONInput from 'react-json-editor-ajrm'
+import locale from 'react-json-editor-ajrm/locale/en'
+import SelectDropdown from '../../../components/SelectDropdown/SelectDropdown'
+import FormsyForm from 'appirio-tech-react-components/components/Formsy'
+const TCFormFields = FormsyForm.Fields
+const Formsy = FormsyForm.Formsy
+
+
+import './TemplateForm.scss'
+
+class TemplateForm extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ valid: false,
+ fields: [],
+ textAreaValid: true,
+ productCategories: [],
+ projectTypes: [],
+ values: null,
+ dirty: false,
+ isFocused: false,
+ isChange: false,
+ showDeleteConfirm: false,
+ primaryKeyType: '',
+ primaryKeyValue: null,
+ verifyPrimaryKeyValue: null,
+ forcedError: {
+ verifyPrimaryKeyValue: null,
+ }
+ }
+ this.onValid = this.onValid.bind(this)
+ this.onInvalid = this.onInvalid.bind(this)
+ this.onChange = this.onChange.bind(this)
+ this.onSave = this.onSave.bind(this)
+ this.onDuplicate = this.onDuplicate.bind(this)
+ this.showDelete = this.showDelete.bind(this)
+ this.cancelDelete = this.cancelDelete.bind(this)
+ this.confirmDelete = this.confirmDelete.bind(this)
+ this.onChangeDropdown = this.onChangeDropdown.bind(this)
+ this.onVerifyPrimaryKeyValueChange = this.onVerifyPrimaryKeyValueChange.bind(this)
+ this.init = this.init.bind(this)
+ }
+
+ componentDidMount() {
+ // this.init(this.props)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.init(nextProps)
+ }
+
+ componentWillMount() {
+ this.init(this.props)
+ }
+
+ init(props) {
+ const { metadata, metadataType, isNew } = props
+ const name = metadataType
+ const type = metadata && metadata.hasOwnProperty('id') ? 'number' : 'text'
+ let primaryKeyValue = null
+ primaryKeyValue = metadata && metadata.hasOwnProperty('id') ? metadata['id'] : null
+ primaryKeyValue = metadata && !metadata.hasOwnProperty('id') ? metadata['key'] : primaryKeyValue
+
+ this.setState({
+ // productCategories: metadataType === 'productTemplate' ? this.getProductCategoryOptions() : [],
+ // projectTypes: metadataType === 'projectTemplate' ? this.getProjectTypeOptions() : [],
+ values: isNew && !metadata ? {} : metadata,
+ name,
+ primaryKeyType: type,
+ primaryKeyValue
+ })
+ }
+
+ getField(field, isRequired=true) {
+ const { values } = this.state
+ const validations = null
+ const type = field['type']
+ const label = field['key']
+ const isDropdown = type === 'dropdown'
+ const isObject = type === 'object'
+ const isJSON = type === 'json'
+ const isCheckbox = type === 'checkbox'
+ const isTextBox = !isDropdown && !isCheckbox && !isObject && !isJSON
+ const options = isDropdown ? field['options'] : []
+ let value = field['value']
+ let isReadOnly = false
+ if (values && values[label]) {
+ value = field['type'] === 'object' ? JSON.stringify(values[label]) : values[label]
+ if (values.hasOwnProperty('id') && label === 'id') {
+ if (!this.props.isNew) {
+ isReadOnly = true
+ }
+ }
+ if (!values.hasOwnProperty('id') && label === 'key') {
+ if (!this.props.isNew) {
+ isReadOnly = true
+ }
+ }
+ }
+
+ return (
+
+
{`${!isCheckbox ? label : ''}`}
+ {
+ isTextBox && (
+
+ )
+ }
+ {
+ isDropdown && (
+
+
+
+ )
+ }
+ {
+ isCheckbox && (
+
+
+
+ )
+ }
+ {
+ isObject && (
+
+ )
+ }
+ {
+ isJSON && (
+
+ { this.onJSONEdit(field, params) } }
+ />
+
+ )
+ }
+
+ )
+ }
+
+ onValid() {
+ this.setState({valid: true})
+ }
+
+ onInvalid() {
+ this.setState({valid: false})
+ }
+
+ /**
+ * Validate the id before delete template
+ */
+ validate(state) {
+ const errors = {
+ verifyPrimaryKeyValue: null,
+ }
+
+ if (state.verifyPrimaryKeyValue !== null && state.verifyPrimaryKeyValue !== state.primaryKeyValue.toString()) {
+ errors.verifyPrimaryKeyValue = `The ${state.primaryKeyType === 'number' ? 'id' : 'key'} do not match`
+ }
+ return errors
+ }
+
+ onVerifyPrimaryKeyValueChange(type, value) {
+ const newState = {...this.state,
+ verifyPrimaryKeyValue: value,
+ isFocused: true,
+ }
+ newState.forcedError = this.validate(newState)
+ this.setState(newState)
+ }
+
+ onDuplicate() {
+ this.props.createTemplate(true)
+ }
+
+ onSave() {
+ const { saveTemplate } = this.props
+ const { primaryKeyValue, values } = this.state
+ let payload = values
+
+ if (values.hasOwnProperty('aliases')) {
+ const aliases = _.split(values.aliases, ',')
+ payload = _.assign({}, payload, { aliases })
+ }
+ saveTemplate(primaryKeyValue, payload)
+ }
+
+ showDelete() {
+ this.setState({
+ showDeleteConfirm: true,
+ })
+ }
+
+ cancelDelete() {
+ this.setState({
+ showDeleteConfirm: false,
+ })
+ }
+
+ confirmDelete() {
+ const { forcedError, primaryKeyValue } = this.state
+ if (!forcedError.verifyPrimaryKeyValue) {
+ this.setState({
+ showDeleteConfirm: false
+ })
+ this.props.deleteTemplate(primaryKeyValue)
+ }
+ }
+
+ onChangeDropdown(option) {
+ const { values } = this.state
+ this.setState({
+ values: _.assign({}, values, {category: option.value})
+ })
+ }
+
+ onChange(currentValues, isChanged) {
+ const { changeTemplate } = this.props
+ console.log(currentValues)
+ this.setState({ dirty: isChanged })
+ if (currentValues.hasOwnProperty('metadata')) {
+ try {
+ currentValues.metadata = JSON.parse(currentValues.metadata)
+ this.setState({ textAreaValid: true})
+ changeTemplate(currentValues)
+ } catch (e) {
+ this.setState({ textAreaValid: false})
+ }
+ } else if (currentValues.hasOwnProperty('phases')) {
+ try {
+ currentValues.phases = JSON.parse(currentValues.phases)
+ this.setState({ textAreaValid: true})
+ changeTemplate(currentValues)
+ } catch (e) {
+ this.setState({ textAreaValid: false})
+ }
+ } else {
+ changeTemplate(currentValues)
+ }
+ }
+
+ onJSONEdit(field, { jsObject }) {
+ const { values } = this.state
+ // const type = field['type']
+ const label = field['key']
+ const updatedJSONValue = {}
+ updatedJSONValue[`${label}`] = jsObject
+ this.setState({
+ values: _.assign({}, values, updatedJSONValue)
+ })
+ }
+
+ render() {
+ const { fields } = this.props
+ const {
+ // name,
+ showDeleteConfirm,
+ primaryKeyType,
+ verifyPrimaryKeyValue,
+ forcedError,
+ } = this.state
+ const isRequired = true
+ return (
+
+
+ {
+ _.map(fields, (field) => this.getField(field))
+ }
+
+
+ Save
+
+
+ Duplicate
+
+
+ Delete
+
+
+
+
+
+
+ Are you sure you want to delete this template?
+
+
+ Please enter the {primaryKeyType === 'number' ? 'id' : 'key'} of the template to be deleted
+
+
+
+
+
+
+ Cancel
+ Delete
+
+
+
+
+ )
+ }
+}
+
+TemplateForm.propTypes = {
+ isNew: PropTypes.bool.isRequired,
+ fields: PropTypes.array.isRequired,
+ productCategories: PropTypes.array.isRequired,
+ projectTypes: PropTypes.array.isRequired,
+ metadata: PropTypes.object.isRequired,
+ metadataType: PropTypes.string.isRequired,
+ deleteTemplate: PropTypes.func.isRequired,
+ saveTemplate: PropTypes.func.isRequired,
+ changeTemplate: PropTypes.func.isRequired,
+ createTemplate: PropTypes.func.isRequired,
+ loadProjectMetadata: PropTypes.func.isRequired,
+}
+
+export default TemplateForm
diff --git a/src/routes/metadata/components/TemplateForm.scss b/src/routes/metadata/components/TemplateForm.scss
new file mode 100644
index 000000000..c0ca4f403
--- /dev/null
+++ b/src/routes/metadata/components/TemplateForm.scss
@@ -0,0 +1,79 @@
+// this is to include tc styles in the output library
+@import '~tc-ui/src/styles/tc-includes';
+
+:global {
+ .template-form-container {
+ display: block;
+ position: relative;
+ }
+
+ .template-form {
+ // padding: 0 20px;
+
+ .checkbox {
+ margin-bottom: 10px;
+ }
+
+ > .controls {
+ display: flex;
+ justify-content: space-around;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ text-align: center;
+ }
+ }
+
+ .delete-template-dialog {
+ padding: 50px 40px 50px 40px;
+ top: 50%;
+ left: 50%;
+ right: auto;
+ bottom: auto;
+ transform: translate(-50%, -50%);
+ position: absolute;
+ background: white;
+ box-shadow: 0 2px 9px 0 rgba($tc-gray-90, 0.15);
+ border-radius: 6px;
+ }
+
+ .delete-template-dialog-overlay {
+ background: rgba($tc-gray-90, 0.6);
+ z-index: 20;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ .modal-title {
+ font-family: inherit;
+ font-size: 20px;
+ color: $tc-gray-80;
+ line-height: 30px;
+ }
+
+ .modal-body {
+ font-family: inherit;
+ margin-top: 20px;
+ font-size: 15px;
+ color: $tc-gray-60;
+ line-height: 25px;
+ }
+
+ .dropdown-field {
+ :global(.dropdown-wrap) {
+ margin-left: 0;
+ margin-bottom: 10px;
+ margin-top: 5px;
+ }
+ }
+
+ .textarea-field {
+ :global(.tc-textarea) {
+ max-width: 100%;
+ width: 100%;
+ margin: 0 0 10px 0
+ }
+ }
+}
diff --git a/src/routes/metadata/containers/MetaDataContainer.jsx b/src/routes/metadata/containers/MetaDataContainer.jsx
new file mode 100644
index 000000000..7dd875da2
--- /dev/null
+++ b/src/routes/metadata/containers/MetaDataContainer.jsx
@@ -0,0 +1,166 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ getProductTemplate,
+ saveProductTemplate,
+ deleteProjectsMetadata,
+ createProjectsMetadata,
+ updateProjectsMetadata
+} from '../../../actions/templates'
+import MetaDataPanel from '../components/MetaDataPanel'
+import MetaDataProjectTemplatesGridView from '../components/MetaDataProjectTemplatesGridView'
+import MetaDataProductTemplatesGridView from '../components/MetaDataProductTemplatesGridView'
+import MetaDataProjectTypesGridView from '../components/MetaDataProjectTypesGridView'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+
+import './MetaDataContainer.scss'
+
+class MetaDataContainer extends React.Component {
+
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ if (this.props.templates && !this.props.templates.projectTemplates && !this.props.templates.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ render() {
+ const {
+ loadProjectsMetadata,
+ deleteProjectsMetadata,
+ createProjectsMetadata,
+ updateProjectsMetadata,
+ templates,
+ isAdmin,
+ currentUser,
+ metadataType,
+ match,
+ } = this.props
+ if (metadataType === 'projectTemplates') {
+ const projectTemplates = templates.projectTemplates
+ return (
+
+
+
+ )
+ }
+ if (metadataType === 'projectTemplate') {
+ const projectTemplates = templates.projectTemplates
+ let templateId = match.params.templateId
+ templateId = templateId ? parseInt(templateId) : null
+ const projectTemplate = _.find(projectTemplates, pt => pt.id === templateId)
+ return (
+
+
+
+ )
+ }
+ if (metadataType === 'productTemplates') {
+ const productTemplates = templates.productTemplates
+ return (
+
+
+
+ )
+ }
+ if (metadataType === 'projectTypes') {
+ const projectTypes = templates.projectTypes
+ return (
+
+
+
+ )
+ }
+ return None
+ }
+}
+
+
+
+MetaDataContainer.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ metadataType: PropTypes.string.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProjectsMetadata: PropTypes.func.isRequired,
+ createProjectsMetadata: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ getProductTemplate,
+ saveProductTemplate,
+ deleteProjectsMetadata,
+ createProjectsMetadata,
+ updateProjectsMetadata,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.templates.isLoading || props.templates.projectTemplates)
+const MetaDataContainerWithLoaderEnhanced = enhance(errorHandler(MetaDataContainer))
+const MetaDataContainerWithLoaderAndAuth = requiresAuthentication(MetaDataContainerWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MetaDataContainerWithLoaderAndAuth))
diff --git a/src/routes/metadata/containers/MetaDataContainer.scss b/src/routes/metadata/containers/MetaDataContainer.scss
new file mode 100644
index 000000000..65a84c07b
--- /dev/null
+++ b/src/routes/metadata/containers/MetaDataContainer.scss
@@ -0,0 +1,48 @@
+// this is to include tc styles in the output library
+@import '~tc-ui/src/styles/tc-includes';
+
+:global {
+ .meta-data-container {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 auto;
+ min-width: 960px;
+ padding: 30px 20px 0;
+
+ > .content {
+ flex: 1;
+ margin-right: 30px;
+ max-width: 340px;
+ }
+
+ > .filters {
+ flex: 1;
+ background-color: $tc-white;
+ padding: 10px;
+ border-radius: 6px;
+ }
+
+ h5 {
+ margin-left: 10px;
+ }
+
+ .align-left {
+ margin-left: 10px;
+ margin-bottom: 20px;
+ }
+
+ .input-field {
+ :global(.Dropdown) {
+ overflow-y: scroll;
+ height: 500px;
+ }
+
+ .clear-margin {
+ :global(.dropdown-wrap) {
+ margin-left: 0;
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/src/routes/metadata/containers/ProductCategoriesContainer.jsx b/src/routes/metadata/containers/ProductCategoriesContainer.jsx
new file mode 100644
index 000000000..08257d5c1
--- /dev/null
+++ b/src/routes/metadata/containers/ProductCategoriesContainer.jsx
@@ -0,0 +1,120 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ sortProductCategories
+} from '../../../actions/templates'
+import ProductCategoriesGridView from '../components/ProductCategoriesGridView'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+import CoderBroken from '../../../assets/icons/coder-broken.svg'
+
+import './MetaDataContainer.scss'
+
+class ProductCategoriesContainer extends React.Component {
+
+ constructor(props) {
+ super(props)
+ this.state = { criteria : { sort: 'updatedAt desc' } }
+
+ this.sortHandler = this.sortHandler.bind(this)
+ }
+
+ componentWillMount() {
+ if (!this.props.productCategories && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ sortHandler(fieldName) {
+ this.props.sortProductCategories(fieldName)
+ this.setState({ criteria : { sort: fieldName } })
+ }
+
+ render() {
+ const {
+ productCategories,
+ isLoading,
+ isAdmin,
+ currentUser,
+ error,
+ } = this.props
+ const { criteria } = this.state
+ if (!isAdmin) {
+ return (
+
+
+
+
+ You don't have permission to access Metadata Management
+
+
+
+ )
+ }
+ return (
+
+ )
+ }
+}
+
+
+
+ProductCategoriesContainer.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ productCategories: templates.productCategories,
+ isLoading: templates.isLoading,
+ error: templates.error,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ sortProductCategories,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading || props.templates)
+const ProductCategoriesContainerWithLoaderEnhanced = enhance(errorHandler(ProductCategoriesContainer))
+const ProductCategoriesContainerWithLoaderAndAuth = requiresAuthentication(ProductCategoriesContainerWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProductCategoriesContainerWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProductCategoryDetails.jsx b/src/routes/metadata/containers/ProductCategoryDetails.jsx
new file mode 100644
index 000000000..f94a177a5
--- /dev/null
+++ b/src/routes/metadata/containers/ProductCategoryDetails.jsx
@@ -0,0 +1,111 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ deleteProductCategory,
+ createProductCategory,
+ updateProjectsMetadata,
+} from '../../../actions/templates'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import MetaDataPanel from '../components/MetaDataPanel'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+
+import './MetaDataContainer.scss'
+
+class ProductCategoryDetails extends React.Component {
+
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ render() {
+ const {
+ loadProjectsMetadata,
+ deleteProductCategory,
+ createProductCategory,
+ updateProjectsMetadata,
+ templates,
+ // isLoading,
+ isAdmin,
+ match,
+ } = this.props
+ const productCategories = templates.productCategories
+ const key = match.params.key
+ const productCategory = _.find(productCategories, t => t.key === key)
+ return (
+
+
+
+ )
+ }
+}
+
+
+
+ProductCategoryDetails.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProductCategory: PropTypes.func.isRequired,
+ createProductCategory: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates,
+ isLoading: templates.isLoading,
+ isRemoving: templates.isRemoving,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ deleteProductCategory,
+ createProductCategory,
+ updateProjectsMetadata,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving)
+const ProductCategoryDetailsWithLoaderEnhanced = enhance(errorHandler(ProductCategoryDetails))
+const ProductCategoryDetailsWithLoaderAndAuth = requiresAuthentication(ProductCategoryDetailsWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProductCategoryDetailsWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProductTemplateDetails.jsx b/src/routes/metadata/containers/ProductTemplateDetails.jsx
new file mode 100644
index 000000000..910ed2793
--- /dev/null
+++ b/src/routes/metadata/containers/ProductTemplateDetails.jsx
@@ -0,0 +1,113 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ deleteProductTemplate,
+ updateProjectsMetadata,
+ createProductTemplate,
+} from '../../../actions/templates'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import MetaDataPanel from '../components/MetaDataPanel'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+
+import './MetaDataContainer.scss'
+
+class ProductTemplateDetails extends React.Component {
+
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ render() {
+ const {
+ loadProjectsMetadata,
+ deleteProductTemplate,
+ createProductTemplate,
+ updateProjectsMetadata,
+ templates,
+ // isLoading,
+ isAdmin,
+ match,
+ } = this.props
+ const productTemplates = templates.productTemplates
+ let templateId = match.params.templateId
+ templateId = templateId ? parseInt(templateId) : null
+ const productTemplate = _.find(productTemplates, t => t.id === templateId)
+ return (
+
+
+
+ )
+ }
+}
+
+
+
+ProductTemplateDetails.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProductTemplate: PropTypes.func.isRequired,
+ createProductTemplate: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates,
+ isLoading: templates.isLoading,
+ isRemoving: templates.isRemoving,
+ error: templates.error,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ deleteProductTemplate,
+ createProductTemplate,
+ updateProjectsMetadata,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving)
+const ProductTemplateDetailsWithLoaderEnhanced = enhance(errorHandler(ProductTemplateDetails))
+const ProductTemplateDetailsWithLoaderAndAuth = requiresAuthentication(ProductTemplateDetailsWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProductTemplateDetailsWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProductTemplatesContainer.jsx b/src/routes/metadata/containers/ProductTemplatesContainer.jsx
new file mode 100644
index 000000000..e210cb985
--- /dev/null
+++ b/src/routes/metadata/containers/ProductTemplatesContainer.jsx
@@ -0,0 +1,120 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ sortProductTemplates,
+} from '../../../actions/templates'
+import ProductTemplatesGridView from '../components/ProductTemplatesGridView'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+import CoderBroken from '../../../assets/icons/coder-broken.svg'
+
+import './MetaDataContainer.scss'
+
+class ProductTemplatesContainer extends React.Component {
+
+ constructor(props) {
+ super(props)
+ this.state = { criteria : { sort: 'updatedAt desc' } }
+
+ this.sortHandler = this.sortHandler.bind(this)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ sortHandler(fieldName) {
+ this.props.sortProductTemplates(fieldName)
+ this.setState({ criteria : { sort: fieldName } })
+ }
+
+ render() {
+ const {
+ templates,
+ isLoading,
+ isAdmin,
+ currentUser,
+ error,
+ } = this.props
+ const { criteria } = this.state
+ if (!isAdmin) {
+ return (
+
+
+
+
+ You don't have permission to access Metadata Management
+
+
+
+ )
+ }
+ return (
+
+ )
+ }
+}
+
+
+
+ProductTemplatesContainer.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ sortProductTemplates: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates: templates.productTemplates,
+ isLoading: templates.isLoading,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ sortProductTemplates,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading || props.templates)
+const ProductTemplatesContainerWithLoaderEnhanced = enhance(errorHandler(ProductTemplatesContainer))
+const ProductTemplatesContainerWithLoaderAndAuth = requiresAuthentication(ProductTemplatesContainerWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProductTemplatesContainerWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProjectTemplateDetails.jsx b/src/routes/metadata/containers/ProjectTemplateDetails.jsx
new file mode 100644
index 000000000..945659ec0
--- /dev/null
+++ b/src/routes/metadata/containers/ProjectTemplateDetails.jsx
@@ -0,0 +1,113 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ deleteProjectTemplate,
+ updateProjectsMetadata,
+ createProjectTemplate,
+} from '../../../actions/templates'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import MetaDataPanel from '../components/MetaDataPanel'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+
+import './MetaDataContainer.scss'
+
+class ProjectTemplateDetails extends React.Component {
+
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ render() {
+ const {
+ loadProjectsMetadata,
+ deleteProjectTemplate,
+ createProjectTemplate,
+ updateProjectsMetadata,
+ templates,
+ // isLoading,
+ isAdmin,
+ match,
+ } = this.props
+ const projectTemplates = templates.projectTemplates
+ let templateId = match.params.templateId
+ templateId = templateId ? parseInt(templateId) : null
+ const projectTemplate = _.find(projectTemplates, t => t.id === templateId)
+ return (
+
+
+
+ )
+ }
+}
+
+
+
+ProjectTemplateDetails.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProjectTemplate: PropTypes.func.isRequired,
+ createProjectTemplate: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates,
+ isLoading: templates.isLoading,
+ isRemoving: templates.isRemoving,
+ error: templates.error,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ deleteProjectTemplate,
+ createProjectTemplate,
+ updateProjectsMetadata,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.errorTemp)
+const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving)
+const ProjectTemplateDetailsWithLoaderEnhanced = enhance(errorHandler(ProjectTemplateDetails))
+const ProjectTemplateDetailsWithLoaderAndAuth = requiresAuthentication(ProjectTemplateDetailsWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProjectTemplateDetailsWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProjectTemplatesContainer.jsx b/src/routes/metadata/containers/ProjectTemplatesContainer.jsx
new file mode 100644
index 000000000..03221b02f
--- /dev/null
+++ b/src/routes/metadata/containers/ProjectTemplatesContainer.jsx
@@ -0,0 +1,121 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ sortProjectTemplates,
+} from '../../../actions/templates'
+import ProjectTemplatesGridView from '../components/ProjectTemplatesGridView'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+import CoderBroken from '../../../assets/icons/coder-broken.svg'
+
+import './MetaDataContainer.scss'
+
+class ProjectTemplatesContainer extends React.Component {
+
+ constructor(props) {
+ super(props)
+ this.state = { criteria : { sort: 'updatedAt desc' } }
+
+ this.sortHandler = this.sortHandler.bind(this)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ sortHandler(fieldName) {
+ this.props.sortProjectTemplates(fieldName)
+ this.setState({ criteria : { sort: fieldName } })
+ }
+
+ render() {
+ const {
+ templates,
+ isLoading,
+ isAdmin,
+ currentUser,
+ error,
+ } = this.props
+ const { criteria } = this.state
+ if (!isAdmin) {
+ return (
+
+
+
+
+ You don't have permission to access Metadata Management
+
+
+
+ )
+ }
+ return (
+
+ )
+ }
+}
+
+
+
+ProjectTemplatesContainer.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ sortProjectTemplates: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates: templates.projectTemplates,
+ isLoading: templates.isLoading,
+ error: templates.error,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ sortProjectTemplates,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading || props.templates)
+const ProjectTemplatesContainerWithLoaderEnhanced = enhance(errorHandler(ProjectTemplatesContainer))
+const ProjectTemplatesContainerWithLoaderAndAuth = requiresAuthentication(ProjectTemplatesContainerWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProjectTemplatesContainerWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProjectTypeDetails.jsx b/src/routes/metadata/containers/ProjectTypeDetails.jsx
new file mode 100644
index 000000000..c261146e1
--- /dev/null
+++ b/src/routes/metadata/containers/ProjectTypeDetails.jsx
@@ -0,0 +1,111 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ deleteProjectsMetadata,
+ createProjectType,
+ updateProjectsMetadata,
+} from '../../../actions/templates'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import MetaDataPanel from '../components/MetaDataPanel'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+
+import './MetaDataContainer.scss'
+
+class ProjectTypeDetails extends React.Component {
+
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ if (!this.props.templates && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ render() {
+ const {
+ loadProjectsMetadata,
+ deleteProjectsMetadata,
+ createProjectType,
+ updateProjectsMetadata,
+ templates,
+ // isLoading,
+ isAdmin,
+ match,
+ } = this.props
+ const projectTypes = templates.projectTypes
+ const key = match.params.key
+ const projectType = _.find(projectTypes, t => t.key === key)
+ return (
+
+
+
+ )
+ }
+}
+
+
+
+ProjectTypeDetails.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+ deleteProjectsMetadata: PropTypes.func.isRequired,
+ createProjectType: PropTypes.func.isRequired,
+ updateProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ templates,
+ isLoading: templates.isLoading,
+ isRemoving: templates.isRemoving,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ deleteProjectsMetadata,
+ createProjectType,
+ updateProjectsMetadata,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving)
+const ProjectTypeDetailsWithLoaderEnhanced = enhance(errorHandler(ProjectTypeDetails))
+const ProjectTypeDetailsWithLoaderAndAuth = requiresAuthentication(ProjectTypeDetailsWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProjectTypeDetailsWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/ProjectTypesContainer.jsx b/src/routes/metadata/containers/ProjectTypesContainer.jsx
new file mode 100644
index 000000000..72a1b3b29
--- /dev/null
+++ b/src/routes/metadata/containers/ProjectTypesContainer.jsx
@@ -0,0 +1,120 @@
+/**
+ * Container component for MetaData
+ */
+import React from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { branch, renderComponent, compose, withProps } from 'recompose'
+import {
+ loadProjectsMetadata,
+ sortProjectTypes
+} from '../../../actions/templates'
+import ProjectTypesGridView from '../components/ProjectTypesGridView'
+import spinnerWhileLoading from '../../../components/LoadingSpinner'
+import CoderBot from '../../../components/CoderBot/CoderBot'
+import { requiresAuthentication } from '../../../components/AuthenticatedComponent'
+import {
+ ROLE_ADMINISTRATOR,
+ ROLE_CONNECT_ADMIN,
+} from '../../../config/constants'
+import _ from 'lodash'
+import CoderBroken from '../../../assets/icons/coder-broken.svg'
+
+import './MetaDataContainer.scss'
+
+class ProjectTypesContainer extends React.Component {
+
+ constructor(props) {
+ super(props)
+ this.state = { criteria : { sort: 'updatedAt desc' } }
+
+ this.sortHandler = this.sortHandler.bind(this)
+ }
+
+ componentWillMount() {
+ if (!this.props.projectTypes && !this.props.isLoading) {
+ this.props.loadProjectsMetadata()
+ }
+ }
+
+ sortHandler(fieldName) {
+ this.props.sortProjectTypes(fieldName)
+ this.setState({ criteria : { sort: fieldName } })
+ }
+
+ render() {
+ const {
+ projectTypes,
+ isLoading,
+ isAdmin,
+ currentUser,
+ error,
+ } = this.props
+ const { criteria } = this.state
+ if (!isAdmin) {
+ return (
+
+
+
+
+ You don't have permission to access Metadata Management
+
+
+
+ )
+ }
+ return (
+
+ )
+ }
+}
+
+
+
+ProjectTypesContainer.propTypes = {
+ isAdmin: PropTypes.bool.isRequired,
+ loadProjectsMetadata: PropTypes.func.isRequired,
+}
+
+
+const mapStateToProps = ({ templates, loadUser }) => {
+ const powerUserRoles = [ROLE_ADMINISTRATOR, ROLE_CONNECT_ADMIN]
+
+ return {
+ projectTypes: templates.projectTypes,
+ isLoading: templates.isLoading,
+ error: templates.error,
+ currentUser: loadUser.user,
+ isAdmin: _.intersection(loadUser.user.roles, powerUserRoles).length !== 0
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectsMetadata,
+ sortProjectTypes,
+}
+
+const page500 = compose(
+ withProps({code:500})
+)
+const showErrorMessageIfError = hasLoaded =>
+ branch(hasLoaded, renderComponent(page500(CoderBot)), t => t)
+const errorHandler = showErrorMessageIfError(props => props.error)
+const enhance = spinnerWhileLoading(props => !props.isLoading || props.templates)
+const ProjectTypesContainerWithLoaderEnhanced = enhance(errorHandler(ProjectTypesContainer))
+const ProjectTypesContainerWithLoaderAndAuth = requiresAuthentication(ProjectTypesContainerWithLoaderEnhanced)
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ProjectTypesContainerWithLoaderAndAuth))
\ No newline at end of file
diff --git a/src/routes/metadata/containers/SecondaryToolBarContainer.jsx b/src/routes/metadata/containers/SecondaryToolBarContainer.jsx
new file mode 100644
index 000000000..a91e51b30
--- /dev/null
+++ b/src/routes/metadata/containers/SecondaryToolBarContainer.jsx
@@ -0,0 +1,42 @@
+/**
+ * Secondary toolbar container for project details pages
+ */
+import React from 'react'
+import { withRouter } from 'react-router-dom'
+import { connect } from 'react-redux'
+import PT from 'prop-types'
+
+import GenericMenu from 'components/GenericMenu'
+
+const SecondaryToolBarContainer = ({
+ isMetaDataLoading,
+ metadata,
+}) => {
+ // we only know which menu items to render when we know project version
+ if (isMetaDataLoading || !metadata) {
+ return null
+ }
+
+ // choose set of menu links based on the project version
+ const navLinks = [
+ { label: 'Project Templates', to: '/metadata/projectTemplates' },
+ { label: 'Product Templates', to: '/metadata/productTemplates' },
+ { label: 'Project Types', to: '/metadata/projectTypes' },
+ { label: 'Product Categories', to: '/metadata/productCategories' },
+ ]
+
+ return (
+
+ )
+}
+
+SecondaryToolBarContainer.propTypes = {
+ project: PT.object,
+}
+
+const mapStateToProps = (state) => ({
+ isMetaDataLoading: state.templates.isLoading,
+ metadata: state.templates,
+})
+
+export default connect(mapStateToProps)(withRouter(SecondaryToolBarContainer))
diff --git a/src/routes/metadata/routes.jsx b/src/routes/metadata/routes.jsx
new file mode 100644
index 000000000..135ad3a8c
--- /dev/null
+++ b/src/routes/metadata/routes.jsx
@@ -0,0 +1,51 @@
+/**
+ * Metadata routes
+ */
+import React from 'react'
+import { Route, Switch } from 'react-router-dom'
+import { withProps } from 'recompose'
+import { renderApp } from '../../components/App/App'
+import CoderBot from '../../components/CoderBot/CoderBot'
+import TopBarContainer from '../../components/TopBar/TopBarContainer'
+import MetaDataToolBar from './components/MetaDataToolBar'
+import MetaDataLayout from './components/MetaDataLayout'
+import ProjectTemplatesContainer from './containers/ProjectTemplatesContainer'
+import ProjectTemplateDetails from './containers/ProjectTemplateDetails'
+import ProductTemplateDetails from './containers/ProductTemplateDetails'
+import ProjectTypesContainer from './containers/ProjectTypesContainer'
+import ProjectTypeDetails from './containers/ProjectTypeDetails'
+import ProductTemplatesContainer from './containers/ProductTemplatesContainer'
+import ProductCategoriesContainer from './containers/ProductCategoriesContainer'
+import ProductCategoryDetails from './containers/ProductCategoryDetails'
+import { requiresAuthentication } from '../../components/AuthenticatedComponent'
+
+const MetaDataLayoutWithAuth = requiresAuthentication(MetaDataLayout)
+
+const MetaDataContainerWithAuth = withProps({ main:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+})(MetaDataLayoutWithAuth)
+
+export default (
+ (
+
+ , )} />
+
+ )}
+ />
+)