Skip to content

Commit

Permalink
Integrating react-markdown with miq-structured-list and adding a mark…
Browse files Browse the repository at this point in the history
…down preview
  • Loading branch information
jeffibm committed Nov 30, 2023
1 parent a250f5e commit 457607c
Show file tree
Hide file tree
Showing 62 changed files with 1,944 additions and 440 deletions.
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

0 comments on commit 457607c

Please sign in to comment.