From 6edf03d5c083153df29ceafd07e0d933ca76c7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Pra=C5=BE=C3=A1k?= Date: Tue, 26 Oct 2021 09:21:28 +0200 Subject: [PATCH] Fixes #32930 - Add new OVAL content page (#486) --- webpack/components/EmptyState.js | 5 +- webpack/components/IndexLayout.js | 15 +- webpack/components/IndexTable/index.js | 2 +- webpack/components/LinkButton.js | 26 ++++ webpack/components/withLoading.js | 24 ++- webpack/helpers/formFieldsHelper.js | 63 ++++++++ webpack/helpers/pathsHelper.js | 4 + .../OvalContentsIndex/OvalContentsIndex.js | 10 ++ .../OvalContentsIndex/OvalContentsTable.js | 18 ++- .../__tests__/OvalContentsDestroy.test.js | 4 + .../__tests__/OvalContentsIndex.test.js | 23 ++- .../OvalContentsNew/OvalContentsNew.js | 138 ++++++++++++++++++ .../OvalContentsNew/OvalContentsNew.scss | 3 + .../OvalContentsNew/OvalContentsNewHelper.js | 73 +++++++++ .../__tests__/OvalContentsNew.test.js | 104 +++++++++++++ .../OvalContents/OvalContentsNew/index.js | 13 ++ webpack/routes/routes.js | 7 + 17 files changed, 517 insertions(+), 15 deletions(-) create mode 100644 webpack/components/LinkButton.js create mode 100644 webpack/helpers/formFieldsHelper.js create mode 100644 webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js create mode 100644 webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss create mode 100644 webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js create mode 100644 webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js create mode 100644 webpack/routes/OvalContents/OvalContentsNew/index.js diff --git a/webpack/components/EmptyState.js b/webpack/components/EmptyState.js index 14a064e4..105cf48e 100644 --- a/webpack/components/EmptyState.js +++ b/webpack/components/EmptyState.js @@ -29,7 +29,7 @@ const EmptyStateIcon = ({ error, search, lock }) => { return ; }; -const EmptyState = ({ title, body, error, search, lock }) => ( +const EmptyState = ({ title, body, error, search, lock, primaryButton }) => ( @@ -37,6 +37,7 @@ const EmptyState = ({ title, body, error, search, lock }) => ( {title} {body} + {primaryButton} ); @@ -59,6 +60,7 @@ EmptyState.propTypes = { error: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.string]), search: PropTypes.bool, lock: PropTypes.bool, + primaryButton: PropTypes.node, }; EmptyState.defaultProps = { @@ -68,6 +70,7 @@ EmptyState.defaultProps = { error: undefined, search: false, lock: false, + primaryButton: null, }; export default EmptyState; diff --git a/webpack/components/IndexLayout.js b/webpack/components/IndexLayout.js index bad42b5b..3a998d86 100644 --- a/webpack/components/IndexLayout.js +++ b/webpack/components/IndexLayout.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; +import ToastsList from 'foremanReact/components/ToastsList'; import { Grid, GridItem, @@ -11,25 +12,31 @@ import { import './IndexLayout.scss'; -const IndexLayout = ({ pageTitle, children }) => ( +const IndexLayout = ({ pageTitle, children, contentWidthSpan }) => ( {pageTitle} + - + {pageTitle} - {children} + {children} ); IndexLayout.propTypes = { pageTitle: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.object]).isRequired, + contentWidthSpan: PropTypes.number, +}; + +IndexLayout.defaultProps = { + contentWidthSpan: 12, }; export default IndexLayout; diff --git a/webpack/components/IndexTable/index.js b/webpack/components/IndexTable/index.js index c4c340bf..c1f7a0e0 100644 --- a/webpack/components/IndexTable/index.js +++ b/webpack/components/IndexTable/index.js @@ -52,7 +52,7 @@ const IndexTable = ({ IndexTable.propTypes = { history: PropTypes.object.isRequired, pagination: PropTypes.object.isRequired, - toolbarBtns: PropTypes.array, + toolbarBtns: PropTypes.node, totalCount: PropTypes.number.isRequired, ariaTableLabel: PropTypes.string.isRequired, columns: PropTypes.array.isRequired, diff --git a/webpack/components/LinkButton.js b/webpack/components/LinkButton.js new file mode 100644 index 00000000..b2cb375c --- /dev/null +++ b/webpack/components/LinkButton.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Button } from '@patternfly/react-core'; +import PropTypes from 'prop-types'; + +const LinkButton = ({ path, btnVariant, btnText, isDisabled }) => ( + + + +); + +LinkButton.propTypes = { + path: PropTypes.string.isRequired, + btnText: PropTypes.string.isRequired, + btnVariant: PropTypes.string, + isDisabled: PropTypes.bool, +}; + +LinkButton.defaultProps = { + btnVariant: 'primary', + isDisabled: false, +}; + +export default LinkButton; diff --git a/webpack/components/withLoading.js b/webpack/components/withLoading.js index 5b4a0e93..f7d46a52 100644 --- a/webpack/components/withLoading.js +++ b/webpack/components/withLoading.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { translate as __ } from 'foremanReact/common/I18n'; import Loading from 'foremanReact/components/Loading'; @@ -29,9 +29,17 @@ const withLoading = Component => { renameData, emptyStateTitle, permissions, + primaryButton, + shouldRefetch, ...rest }) => { - const { loading, error, data } = fetchFn(rest); + const { loading, error, data, refetch } = fetchFn(rest); + + useEffect(() => { + if (shouldRefetch) { + refetch(); + } + }, [shouldRefetch, refetch]); if (loading) { return ; @@ -62,7 +70,13 @@ const withLoading = Component => { const result = pluckData(data, resultPath); if ((Array.isArray(result) && result.length === 0) || !result) { - return ; + return ( + + ); } return ; @@ -74,11 +88,15 @@ const withLoading = Component => { renameData: PropTypes.func, emptyStateTitle: PropTypes.string.isRequired, permissions: PropTypes.array, + primaryButton: PropTypes.node, + shouldRefetch: PropTypes.bool, }; Subcomponent.defaultProps = { renameData: data => data, permissions: [], + primaryButton: null, + shouldRefetch: false, }; return Subcomponent; diff --git a/webpack/helpers/formFieldsHelper.js b/webpack/helpers/formFieldsHelper.js new file mode 100644 index 00000000..1fbc78f9 --- /dev/null +++ b/webpack/helpers/formFieldsHelper.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { FormGroup, TextInput } from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; + +const wrapFieldProps = fieldProps => { + const { onChange } = fieldProps; + // modify onChange args to correctly wire formik with pf4 input handlers + const wrappedOnChange = (value, event) => { + onChange(event); + }; + + return { ...fieldProps, onChange: wrappedOnChange }; +}; + +const shouldValidate = (form, fieldName) => { + if (form.touched[fieldName]) { + return form.errors[fieldName] ? 'error' : 'success'; + } + + return 'noval'; +}; + +const fieldWithHandlers = Component => { + const Subcomponent = ({ label, form, field, isRequired, ...rest }) => { + const fieldProps = wrapFieldProps(field); + const valid = shouldValidate(form, field.name); + + return ( + } + validated={valid} + > + + + ); + }; + + Subcomponent.propTypes = { + form: PropTypes.object.isRequired, + field: PropTypes.object.isRequired, + label: PropTypes.string.isRequired, + isRequired: PropTypes.bool, + }; + + Subcomponent.defaultProps = { + isRequired: false, + }; + + return Subcomponent; +}; + +export const TextField = fieldWithHandlers(TextInput); diff --git a/webpack/helpers/pathsHelper.js b/webpack/helpers/pathsHelper.js index 93f348ed..d4e5982a 100644 --- a/webpack/helpers/pathsHelper.js +++ b/webpack/helpers/pathsHelper.js @@ -3,6 +3,7 @@ import { decodeId } from './globalIdHelper'; const experimental = path => `/experimental${path}`; const showPath = path => `${path}/:id`; +const newPath = path => `${path}/new`; export const modelPath = (basePath, model) => `${basePath}/${decodeId(model)}`; @@ -14,8 +15,11 @@ export const resolvePath = (path, params) => path ); +export const ovalContentsApiPath = '/api/v2/compliance/oval_contents'; + export const ovalContentsPath = experimental('/compliance/oval_contents'); export const ovalContentsShowPath = showPath(ovalContentsPath); +export const ovalContentsNewPath = newPath(ovalContentsPath); export const ovalPoliciesPath = experimental('/compliance/oval_policies'); export const ovalPoliciesShowPath = `${showPath(ovalPoliciesPath)}/:tab?`; export const hostsPath = '/hosts'; diff --git a/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js b/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js index fe2109c3..313149ed 100644 --- a/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js +++ b/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsIndex.js @@ -4,7 +4,9 @@ import { useQuery } from '@apollo/client'; import { translate as __ } from 'foremanReact/common/I18n'; import IndexLayout from '../../../components/IndexLayout'; +import LinkButton from '../../../components/LinkButton'; import OvalContentsTable from './OvalContentsTable'; +import { ovalContentsNewPath } from '../../../helpers/pathsHelper'; import { useParamsToVars, useCurrentPagination, @@ -48,6 +50,13 @@ const OvalContentsIndex = props => { deleteOvalContentMutation, __('OVAL Content') )} + primaryButton={ + + } + shouldRefetch={props.location?.state?.refreshOvalContents} /> ); @@ -56,6 +65,7 @@ const OvalContentsIndex = props => { OvalContentsIndex.propTypes = { history: PropTypes.object.isRequired, showToast: PropTypes.func.isRequired, + location: PropTypes.object.isRequired, }; export default OvalContentsIndex; diff --git a/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js b/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js index 355c5b2b..5366f18c 100644 --- a/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js +++ b/webpack/routes/OvalContents/OvalContentsIndex/OvalContentsTable.js @@ -1,13 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; import { translate as __ } from 'foremanReact/common/I18n'; +import { Button } from '@patternfly/react-core'; import withLoading from '../../../components/withLoading'; import withDeleteModal from '../../../components/withDeleteModal'; import IndexTable from '../../../components/IndexTable'; +import { + ovalContentsNewPath, + ovalContentsPath, + modelPath, +} from '../../../helpers/pathsHelper'; import { linkCell } from '../../../helpers/tableHelper'; -import { ovalContentsPath, modelPath } from '../../../helpers/pathsHelper'; const OvalContentsTable = props => { const columns = [ @@ -43,6 +48,16 @@ const OvalContentsTable = props => { return actions; }; + const createBtn = ( + + ); + return ( { totalCount={props.totalCount} history={props.history} ariaTableLabel={__('OVAL Contents table')} + toolbarBtns={createBtn} /> ); }; diff --git a/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js b/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js index 7298c78b..e27c73a2 100644 --- a/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js +++ b/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsDestroy.test.js @@ -28,6 +28,7 @@ describe('OvalContentsIndex', () => { render( @@ -55,6 +56,7 @@ describe('OvalContentsIndex', () => { render( @@ -80,6 +82,7 @@ describe('OvalContentsIndex', () => { render( { render( diff --git a/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js b/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js index 1b9e410a..dcd5d737 100644 --- a/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js +++ b/webpack/routes/OvalContents/OvalContentsIndex/__tests__/OvalContentsIndex.test.js @@ -33,7 +33,7 @@ const TestComponent = withRedux( describe('OvalContentsIndex', () => { it('should load page', async () => { const { container } = render( - + ); expect(screen.getByText('Loading')).toBeInTheDocument(); await waitFor(tick); @@ -55,6 +55,7 @@ describe('OvalContentsIndex', () => { const { container } = render( ); @@ -72,14 +73,18 @@ describe('OvalContentsIndex', () => { ); }); it('should show empty state', async () => { - render(); + render( + + ); expect(screen.getByText('Loading')).toBeInTheDocument(); await waitFor(tick); expect(screen.queryByText('Loading')).not.toBeInTheDocument(); expect(screen.getByText('No OVAL Contents found.')).toBeInTheDocument(); }); it('should show errors', async () => { - render(); + render( + + ); expect(screen.getByText('Loading')).toBeInTheDocument(); await waitFor(tick); expect(screen.queryByText('Loading')).not.toBeInTheDocument(); @@ -89,13 +94,21 @@ describe('OvalContentsIndex', () => { expect(screen.getByText('Error!')).toBeInTheDocument(); }); it('should load page for user with permissions', async () => { - render(); + render( + + ); await waitFor(tick); expect(screen.queryByText('Loading')).not.toBeInTheDocument(); expect(screen.getByText('ansible OVAL content')).toBeInTheDocument(); }); it('should not load page for user without permissions', async () => { - render(); + render( + + ); await waitFor(tick); expect(screen.queryByText('Loading')).not.toBeInTheDocument(); expect(screen.queryByText('ansible OVAL content')).not.toBeInTheDocument(); diff --git a/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js new file mode 100644 index 00000000..27096754 --- /dev/null +++ b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.js @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { Formik, Field as FormikField } from 'formik'; + +import { + Form as PfForm, + ActionGroup, + Button, + FileUpload, + FormGroup, + Radio, + Spinner, +} from '@patternfly/react-core'; +import { + onSubmit, + createValidationSchema, + validateFile, + submitDisabled, +} from './OvalContentsNewHelper'; +import LinkButton from '../../../components/LinkButton'; +import IndexLayout from '../../../components/IndexLayout'; +import { TextField } from '../../../helpers/formFieldsHelper'; +import { ovalContentsPath } from '../../../helpers/pathsHelper'; + +import './OvalContentsNew.scss'; + +const OvalContentsNew = props => { + const [file, setFile] = useState(null); + const [fileTouched, setFileTouched] = useState(false); + const [fileFromUrl, setFileFromUrl] = useState(true); + + const handleFileChange = (value, filename, event) => { + setFile(value); + setFileTouched(true); + }; + + return ( + + + onSubmit( + values, + actions, + props.showToast, + props.history, + fileFromUrl, + file + ) + } + initialValues={{ name: '', url: '' }} + validationSchema={createValidationSchema(fileFromUrl)} + > + {formProps => ( + + + + { + setFileFromUrl(true); + // Force validations to run by setting the same value. + // Workaround for https://github.com/formium/formik/issues/1755 + formProps.setFieldValue(formProps.values.url); + }} + label={__('OVAL Content from URL')} + /> + { + setFileFromUrl(false); + const filtered = Object.entries(formProps.errors).filter( + ([key, value]) => key !== 'url' + ); + formProps.setErrors(Object.fromEntries(filtered)); + }} + label={__('OVAL Content from file')} + /> + + {!fileFromUrl ? ( + + + + ) : ( + + )} + + + + {formProps.isSubmitting ? : null} + + + )} + + + ); +}; + +OvalContentsNew.propTypes = { + showToast: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, +}; + +export default OvalContentsNew; diff --git a/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss new file mode 100644 index 00000000..d71d1b62 --- /dev/null +++ b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNew.scss @@ -0,0 +1,3 @@ +#scap-file-source-url, #scap-file-source-file { + margin: 0; +} diff --git a/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js new file mode 100644 index 00000000..ee69caf4 --- /dev/null +++ b/webpack/routes/OvalContents/OvalContentsNew/OvalContentsNewHelper.js @@ -0,0 +1,73 @@ +import * as Yup from 'yup'; + +import api from 'foremanReact/redux/API/API'; +import { prepareErrors } from 'foremanReact/redux/actions/common/forms'; +import { sprintf, translate as __ } from 'foremanReact/common/I18n'; +import { + ovalContentsPath, + ovalContentsApiPath, +} from '../../../helpers/pathsHelper'; + +export const submitForm = (params, actions) => { + const headers = { + 'Content-Type': 'multipart/form-data', + }; + return api.post(ovalContentsApiPath, params, headers); +}; + +export const onSubmit = async ( + values, + actions, + showToast, + history, + fileFromUrl, + file +) => { + const formData = new FormData(); + if (fileFromUrl) { + formData.append('oval_content[url]', values.url); + } else { + formData.append('oval_content[scap_file]', file); + } + formData.append('oval_content[name]', values.name); + try { + await submitForm(formData, actions); + history.push(ovalContentsPath, { refreshOvalContents: true }); + showToast({ + type: 'success', + message: sprintf(__('OVAL Content %s successfully created'), values.name), + }); + } catch (error) { + onError(error, actions, showToast); + } +}; + +const onError = (error, actions, showToast) => { + actions.setSubmitting(false); + if (error.response?.status === 422) { + actions.setErrors(prepareErrors(error?.response?.data?.error?.errors, {})); + } else { + showToast({ + type: 'error', + message: __( + 'Unknown error when submitting data, please try again later.' + ), + }); + } +}; + +export const validateFile = (file, touched) => { + if (!touched) { + return 'default'; + } + return file ? 'success' : 'error'; +}; + +export const submitDisabled = (formProps, file, fileFromUrl) => + formProps.isSubmitting || !formProps.isValid || (!fileFromUrl && !file); + +export const createValidationSchema = contentFromUrl => + Yup.object().shape({ + name: Yup.string().required("can't be blank"), + ...(contentFromUrl && { url: Yup.string().required("can't be blank") }), + }); diff --git a/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js b/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js new file mode 100644 index 00000000..dd68e77c --- /dev/null +++ b/webpack/routes/OvalContents/OvalContentsNew/__tests__/OvalContentsNew.test.js @@ -0,0 +1,104 @@ +import React from 'react'; + +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import api from 'foremanReact/redux/API/API'; + +import OvalContentsNew from '../OvalContentsNew'; +import { withRouter, withRedux, tick } from '../../../../testHelper'; +import { ovalContentsPath } from '../../../../helpers/pathsHelper'; + +jest.mock('foremanReact/redux/API/API', () => ({ post: jest.fn() })); + +const TestComponent = withRouter(withRedux(OvalContentsNew)); + +describe('OvalContentsNew', () => { + it('should create with content from URL', async () => { + const pushMock = jest.fn(); + const toastMock = jest.fn(); + + api.post.mockImplementation(() => Promise.resolve()); + + render( + + ); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('OVAL Content Source')).toBeInTheDocument(); + expect(screen.getByText('URL')).toBeInTheDocument(); + expect(screen.queryByText('File')).not.toBeInTheDocument(); + expect(screen.getByText('Submit')).toBeDisabled(); + userEvent.type(screen.getByLabelText('name'), 'test content'); + await waitFor(tick); + expect(screen.getByText('Submit')).toBeDisabled(); + userEvent.type( + screen.getByLabelText(/url/), + 'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2' + ); + await waitFor(tick); + expect(screen.getByText('Submit')).not.toBeDisabled(); + userEvent.click(screen.getByText('Submit')); + await waitFor(tick); + expect(pushMock).toHaveBeenCalledWith(ovalContentsPath, { + refreshOvalContents: true, + }); + expect(toastMock).toHaveBeenCalledWith({ + type: 'success', + message: 'OVAL Content test content successfully created', + }); + }); + it('should show resource errors', async () => { + const pushMock = jest.fn(); + const toastMock = jest.fn(); + api.post.mockImplementation(() => { + // eslint-disable-next-line no-throw-literal + throw { + response: { + status: 422, + data: { error: { errors: { name: ['has already been taken'] } } }, + }, + }; + }); + + render( + + ); + userEvent.type(screen.getByLabelText('name'), 'test content'); + userEvent.type( + screen.getByLabelText(/url/), + 'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2' + ); + await waitFor(tick); + userEvent.click(screen.getByText('Submit')); + await waitFor(tick); + expect(pushMock).not.toHaveBeenCalled(); + expect(screen.getByText('has already been taken')).toBeInTheDocument(); + }); + it('should show error toast on unexpected error', async () => { + const pushMock = jest.fn(); + const toastMock = jest.fn(); + + api.post.mockImplementation(() => { + // eslint-disable-next-line no-throw-literal + throw { response: { status: 500 } }; + }); + + render( + + ); + userEvent.type(screen.getByLabelText('name'), 'test content'); + userEvent.type( + screen.getByLabelText(/url/), + 'http://oval-content-source.org/security/data/oval/v2/CentOS7/ansible-2.9.oval.xml.bz2' + ); + await waitFor(tick); + userEvent.click(screen.getByText('Submit')); + await waitFor(tick); + expect(pushMock).not.toHaveBeenCalled(); + expect(screen.getByText('Submit')).not.toBeDisabled(); + expect(toastMock).toHaveBeenCalledWith({ + type: 'error', + message: 'Unknown error when submitting data, please try again later.', + }); + }); +}); diff --git a/webpack/routes/OvalContents/OvalContentsNew/index.js b/webpack/routes/OvalContents/OvalContentsNew/index.js new file mode 100644 index 00000000..0a8f2494 --- /dev/null +++ b/webpack/routes/OvalContents/OvalContentsNew/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { showToast } from '../../../helpers/toastHelper'; + +import OvalContentsNew from './OvalContentsNew'; + +const WrappedOvalContentsNew = props => { + const dispatch = useDispatch(); + + return ; +}; + +export default WrappedOvalContentsNew; diff --git a/webpack/routes/routes.js b/webpack/routes/routes.js index 97e44792..69b39e49 100644 --- a/webpack/routes/routes.js +++ b/webpack/routes/routes.js @@ -1,12 +1,14 @@ import React from 'react'; import OvalContentsIndex from './OvalContents/OvalContentsIndex'; import OvalContentsShow from './OvalContents/OvalContentsShow'; +import OvalContentsNew from './OvalContents/OvalContentsNew'; import OvalPoliciesIndex from './OvalPolicies/OvalPoliciesIndex'; import OvalPoliciesShow from './OvalPolicies/OvalPoliciesShow'; import { ovalContentsPath, ovalContentsShowPath, + ovalContentsNewPath, ovalPoliciesPath, ovalPoliciesShowPath, } from '../helpers/pathsHelper'; @@ -17,6 +19,11 @@ export default [ render: props => , exact: true, }, + { + path: ovalContentsNewPath, + render: props => , + exact: true, + }, { path: ovalContentsShowPath, render: props => ,