Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrating react-markdown with miq-structured-list and adding a markdown preview to service-catalog-item's long-description field #8972

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/helpers/catalog_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def service_catalog_summary(record, sb_data)
{:cells => image},
row_data(_('Name'), record.name),
row_data(_('Description'), record.description),
row_data(_('Long Description'), record.long_description),
row_data(_('Long Description'), {:input => 'markdown', :props => {:content => record.long_description}}),
row_data(_('Dialog'), sb_data[:dialog_label]),
]
if record.currency && record.price
Expand Down Expand Up @@ -174,8 +174,8 @@ def catalog_custom_image(record)
end

def catalog_details(record)
data = {:title => _('Details'), :mode => "miq_catalog_details"}
data[:rows] = [row_data(_('Long Description'), record.long_description)]
data = {:title => _('Long Description'), :mode => "miq_catalog_details"}
data[:rows] = [row_data('', {:input => 'markdown', :props => {:content => record.long_description}})]
miq_structured_list(data)
end

Expand Down
8 changes: 8 additions & 0 deletions app/javascript/components/MarkdownPreview/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Configuration to idendify the usage of the Markdown component and the props needed. */
export const previewConfiguration = {
CATALOG_EDIT_LONG_DESCRIPTION: {
title: __('Long Description'),
field: 'long_description',
mode: 'htmlmixed',
},
};
98 changes: 98 additions & 0 deletions app/javascript/components/MarkdownPreview/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Controlled as CodeMirror } from 'react-codemirror2';
import MiqMarkdown from '../MiqMarkdown';
import { previewConfiguration } from './helper';

/** Component to preview the markdown contents on-the-fly. */
const MarkdownPreview = ({
content, url, type,
}) => {
const miqCustomTabReducer = useSelector((state) => state.miqCustomTabReducer);
const { title, mode, field } = previewConfiguration[type];

const [data, setData] = useState({
editorContent: content,
oneTrans: 0,
});

/** The textField present in the ruby form has to be updated when data in the CodeMirror is changed. */
useEffect(() => {
const textArea = document.getElementById(field);
textArea.value = data.editorContent;
if (data.oneTrans === 1) {
// To enable the form's save/cancel buttons (w.r.t pervious code-mirror implementation).
window.miqSendOneTrans(url);
}
}, [data.editorContent]);

/** The code-mirror component needs to be refreshed when the tab selection is changed.
* If this is not used, then the code mirror will not load its default value.
*/
useEffect(() => {
window.miq_refresh_code_mirror();
}, [miqCustomTabReducer]);

/** Function to render the title of editor and preview sections. */
const renderTitle = (type) => (
<div className="markdown-section-title">
{`${title} - ${type}`}
</div>
);

/** Function to render the code-mirror editor. */
const renderEditor = () => (
<div className="markdown-section" id="editor">
{renderTitle(__('Editor'))}
<div className="markdown-section-content">
<CodeMirror
className="miq-codemirror miq-structured-list-code-mirror"
options={{
mode,
lineNumbers: true,
matchBrackets: true,
theme: 'eclipse',
viewportMargin: Infinity,
readOnly: false,
}}
onBeforeChange={(_editor, _data, value) => setData({
...data,
editorContent: value,
oneTrans: data.oneTrans + 1,
})}
value={data.editorContent}
/>
</div>
</div>
);

/** Function to render the preview of the data entered in code-mirror editor. */
const renderPreview = () => (
<div className="markdown-section" id="preview">
{renderTitle(__('Preview'))}
<div className="markdown-section-content">
<MiqMarkdown content={data.editorContent} />
</div>
</div>
);

return (
<div className="markdown-wrapper">
{renderEditor()}
{renderPreview()}
</div>
);
};

MarkdownPreview.propTypes = {
content: PropTypes.string,
url: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};

MarkdownPreview.defaultProps = {
content: undefined,
};

export default MarkdownPreview;
16 changes: 16 additions & 0 deletions app/javascript/components/MiqMarkdown/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';

/** Component to render the markdown contents. */
const MiqMarkdown = ({ content }) => <ReactMarkdown>{content}</ReactMarkdown>;

MiqMarkdown.propTypes = {
content: PropTypes.string,
};

MiqMarkdown.defaultProps = {
content: undefined,
};

export default MiqMarkdown;
18 changes: 11 additions & 7 deletions app/javascript/components/miq-custom-tab/index.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Tabs, Tab } from 'carbon-components-react';
import { useDispatch } from 'react-redux';
import { miqCustomTabActions } from '../../miq-redux/actions/miq-custom-tab-actions';

const MiqCustomTab = ({ containerId, tabLabels, type }) => {
const dispatch = useDispatch();
const [data, setData] = useState({ loading: false });
const tabConfigurations = (name) => [
{ type: 'CATALOG_SUMMARY' },
{ type: 'CATALOG_EDIT' },
{ type: 'CATALOG_EDIT', js: () => name === 'detail' && dispatch(miqCustomTabActions.incrementClickCount()) },
{ type: 'CATALOG_REQUEST_INFO', url: `/miq_request/prov_field_changed?tab_id=${name}&edit_mode=true` },
{ type: 'UTILIZATION' },
];
Expand Down Expand Up @@ -37,13 +40,14 @@ const MiqCustomTab = ({ containerId, tabLabels, type }) => {
};

/** Function to load the tab contents which are already available within the page. */
const staticContents = (name) => {
const staticContents = (name, config) => {
const tabs = containerTabs();
tabs.forEach((child) => {
if (child.parentElement.id === containerId) {
child.classList.remove('active');
if (child.id === `${name}`) {
child.classList.add('active');
if (config.js) config.js();
}
}
});
Expand All @@ -53,10 +57,10 @@ const MiqCustomTab = ({ containerId, tabLabels, type }) => {
/** Function to load tab contents after a url is executed.
* After the url is executed, the selected tab contents are displayes using the staticContents function.
*/
const dynamicContents = (name, url) => {
const dynamicContents = (name, config) => {
clearTabContents();
window.miqJqueryRequest(url).then(() => {
staticContents(name);
window.miqJqueryRequest(config.url).then(() => {
staticContents(name, config);
setData({ loading: false });
});
};
Expand All @@ -66,14 +70,14 @@ const MiqCustomTab = ({ containerId, tabLabels, type }) => {
if (!data.loading) {
miqSparkleOn();
const config = configuration(name);
return config && config.url ? dynamicContents(name, config.url) : staticContents(name);
return config && config.url ? dynamicContents(name, config) : staticContents(name, config);
}
return data;
};

/** Function to render the tabs from the tabLabels props */
const renderTabs = () => tabLabels.map(({ name, text }) => (
<Tab key={`tab${name}`} label={text} onClick={() => onTabSelect(name)} />
<Tab key={`tab${name}`} label={`${text}`} onClick={() => onTabSelect(name)} />
));

return (
Expand Down
1 change: 1 addition & 0 deletions app/javascript/components/miq-structured-list/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const InputTypes = {
COMPONENT: 'component',
DROPDOWN: 'dropdown',
CODEMIRROR: 'code_mirror',
MARKDOWN: 'markdown',
};

export const DynamicReactComponents = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Checkbox, TextArea, Dropdown } from 'carbon-components-react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { DynamicReactComponents, InputTypes } from '../../helpers';
import MiqMarkdown from '../../../MiqMarkdown';

/** Component to render textarea / checkbox / react components */
const MiqStructuredListInputs = ({ value, action }) => {
Expand Down Expand Up @@ -50,6 +51,9 @@ const MiqStructuredListInputs = ({ value, action }) => {
value={payload}
/>
);

const renderMarkdownComponent = ({ props: { content } }) => <MiqMarkdown content={content} />;

switch (value.input) {
case InputTypes.TEXTAREA:
return renderTextArea(value);
Expand All @@ -61,6 +65,8 @@ const MiqStructuredListInputs = ({ value, action }) => {
return renderDropDownComponent(value);
case InputTypes.CODEMIRROR:
return renderCodeMirrorComponent(value);
case InputTypes.MARKDOWN:
return renderMarkdownComponent(value);
default:
return null;
}
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/miq-redux/actions/miq-custom-tab-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const incrementClickCount = () => ({
type: 'INCREMENT_CLICK_COUNT',
});

export const miqCustomTabActions = {
incrementClickCount,
};
10 changes: 10 additions & 0 deletions app/javascript/miq-redux/miq-custom-tab-reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const miqCustomTabReducer = (clickCount = 0, action) => {
switch (action.type) {
case 'INCREMENT_CLICK_COUNT':
return clickCount + 1;
default:
return clickCount;
}
};

export default miqCustomTabReducer;
2 changes: 2 additions & 0 deletions app/javascript/miq-redux/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { history } from '../miq-component/react-history.js';

import { notificationReducer } from './notification-reducer';
import formButtonsReducer from '../forms/form-buttons-reducer';
import miqCustomTabReducer from './miq-custom-tab-reducer';

const initialState = {};

Expand All @@ -23,6 +24,7 @@ const initializeStore = () => {
store.asyncReducers = {
FormButtons: formButtonsReducer,
notificationReducer,
miqCustomTabReducer,
};

/**
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/packs/component-definitions-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ import ImportDatastoreViaGit from '../components/automate-import-export-form/imp
import InterfacesForm from '../components/network-routers-interfaces-form';
import ISODatastoreTable from '../components/data-tables/iso-datastore-table';
import LiveMigrateForm from '../components/live-migrate-form';
import MarkdownPreview from '../components/MarkdownPreview';
import MiqAboutModal from '../components/miq-about-modal/miq-about-modal';
import MiqAlertSetForm from '../components/miq-alert-set-form';
import MiqCustomTab from '../components/miq-custom-tab';
import MiqDataTable from '../components/miq-data-table';
import MiqMarkdown from '../components/MiqMarkdown';
import MiqPagination from '../components/miq-pagination';
import MiqStructuredList from '../components/miq-structured-list';
import MiqStructuredListHeader from '../components/miq-structured-list/miq-structured-list-header';
Expand Down Expand Up @@ -233,10 +235,12 @@ ManageIQ.component.addReact('ISODatastoreTable', ISODatastoreTable);
ManageIQ.component.addReact('LiveMigrateForm', LiveMigrateForm);
ManageIQ.component.addReact('menu.MainMenu', MainMenu);
ManageIQ.component.addReact('menu.Navbar', Navbar);
ManageIQ.component.addReact('MarkdownPreview', MarkdownPreview);
ManageIQ.component.addReact('MiqAboutModal', MiqAboutModal);
ManageIQ.component.addReact('MiqAlertSetForm', MiqAlertSetForm);
ManageIQ.component.addReact('MiqCustomTab', MiqCustomTab);
ManageIQ.component.addReact('MiqDataTable', MiqDataTable);
ManageIQ.component.addReact('MiqMarkdown', MiqMarkdown);
ManageIQ.component.addReact('MiqPagination', MiqPagination);
ManageIQ.component.addReact('MiqStructuredList', MiqStructuredList);
ManageIQ.component.addReact('MiqStructuredListHeader', MiqStructuredListHeader);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`Action Form Component should render adding a new action 1`] = `
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`Add/remove security groups form component should add security group 1`]
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down Expand Up @@ -73,6 +74,7 @@ exports[`Add/remove security groups form component should remove security group
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down Expand Up @@ -140,6 +142,7 @@ exports[`Add/remove security groups form component should render add security gr
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down Expand Up @@ -207,6 +210,7 @@ exports[`Add/remove security groups form component should render remove security
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`Ansible Credential Form Component should render adding a new credential
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down Expand Up @@ -1487,6 +1488,7 @@ exports[`Ansible Credential Form Component should render editing a credential 1`
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
},
"dispatch": [Function],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports[`Ansible playbook edit catalog Form Component should not render some fie
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
"tenants_tree": [Function],
},
Expand Down Expand Up @@ -58342,6 +58343,7 @@ exports[`Ansible playbook edit catalog Form Component should render correct form
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
"tenants_tree": [Function],
},
Expand Down Expand Up @@ -119648,6 +119650,7 @@ exports[`Ansible playbook edit catalog Form Component should render retirement p
Object {
"asyncReducers": Object {
"FormButtons": [Function],
"miqCustomTabReducer": [Function],
"notificationReducer": [Function],
"tenants_tree": [Function],
},
Expand Down
Loading