Skip to content

Commit 02edb5a

Browse files
jeffibmjeffbonson
authored andcommitted
Order Service Form conversion
1 parent 8adaffa commit 02edb5a

40 files changed

+17325
-8
lines changed

app/controllers/catalog_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ def svc_catalog_provision
743743
ra, st, svc_catalog_provision_finish_submit_endpoint
744744
)
745745
@in_a_form = true
746+
@dialog_locals = options[:dialog_locals]
746747
replace_right_cell(:action => "dialog_provision", :dialog_locals => options[:dialog_locals])
747748
else
748749
# if catalog item has no dialog and provision button was pressed from list view

app/helpers/catalog_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module CatalogHelper
33
include RequestInfoHelper
44
include Mixins::AutomationMixin
55
include OrchestrationTemplateHelper
6+
include OrderServiceHelper
67

78
def miq_catalog_resource(resources)
89
headers = ["", _("Name"), _("Description"), _("Action Order"), _("Provision Order"), _("Action Start"), _("Action Stop"), _("Delay (mins) Start"), _("Delay (mins) Stop")]

app/helpers/miq_request_helper.rb

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ def row_data(label, value)
77
end
88

99
def request_task_configuration_script_ids(miq_request)
10-
miq_request.miq_request_tasks.map { |task| task.options&.dig(:configuration_script_id) }.compact
10+
miq_request.miq_request_tasks.filter_map { |task| task.options&.dig(:configuration_script_id) }
11+
end
12+
13+
def select_box_options(options)
14+
options.map do |item|
15+
if /Classification::(\d+)/.match?(item)
16+
classification_id = item.match(/Classification::(\d+)/)[1]
17+
classification = Classification.find_by(:id => classification_id)
18+
if classification
19+
{:label => classification.description, :value => classification.id.to_s}
20+
end
21+
else
22+
{:label => item, :value => item}
23+
end
24+
end
25+
end
26+
27+
def dialog_field_values(dialog)
28+
transformed_data = dialog.transform_keys { |key| key.sub('Array::', '') }
29+
transformed_data.transform_values do |value|
30+
if value.to_s.include?("\u001F")
31+
select_box_options(value.split("\u001F"))
32+
elsif value.to_s.include?("::")
33+
model, id = value.split("::")
34+
record = model.constantize.find_by(:id => id)
35+
record ? [{:label => record.description, :value => record.id}] : value
36+
else
37+
value
38+
end
39+
end
40+
end
41+
42+
def service_request_data(request_options)
43+
{
44+
:dialogId => request_options[:workflow_settings][:dialog_id],
45+
:requestDialogOptions => dialog_field_values(request_options[:dialog]),
46+
}
1147
end
1248
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module OrderServiceHelper
2+
def order_service_data(dialog)
3+
{
4+
:dialogId => dialog[:dialog_id],
5+
:params => {
6+
:resourceActionId => dialog[:resource_action_id],
7+
:targetId => dialog[:target_id],
8+
:targetType => dialog[:target_type],
9+
:realTargetType => dialog[:real_target_type],
10+
},
11+
:urls => {
12+
:apiSubmitEndpoint => dialog[:api_submit_endpoint],
13+
:apiAction => dialog[:api_action],
14+
:cancelEndPoint => dialog[:cancel_endpoint],
15+
:finishSubmitEndpoint => dialog[:finish_submit_endpoint],
16+
:openUrl => dialog[:open_url],
17+
}
18+
}
19+
end
20+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useContext } from 'react';
2+
import PropTypes from 'prop-types';
3+
import classNames from 'classnames';
4+
import { DIALOG_FIELD_TYPES } from './constants';
5+
import CheckboxField from './dialogFieldItems/CheckboxField';
6+
import DateField from './dialogFieldItems/DateField';
7+
import DateTimeField from './dialogFieldItems/DateTimeField';
8+
import DropDownField from './dialogFieldItems/DropDownField';
9+
import RadioField from './dialogFieldItems/RadioField';
10+
import RefreshField from './RefreshField';
11+
import ServiceContext from './ServiceContext';
12+
import TagField from './dialogFieldItems/TagField';
13+
import TextInputField from './dialogFieldItems/TextInputField';
14+
import TextAreaField from './dialogFieldItems/TextAreaField';
15+
16+
/** Function to render fields based on type */
17+
const renderFieldContent = (field) => {
18+
switch (field.type) {
19+
case DIALOG_FIELD_TYPES.checkBox:
20+
return <CheckboxField field={field} />;
21+
case DIALOG_FIELD_TYPES.date:
22+
return <DateField field={field} />;
23+
case DIALOG_FIELD_TYPES.dateTime:
24+
return <DateTimeField field={field} />;
25+
case DIALOG_FIELD_TYPES.dropDown:
26+
return <DropDownField field={field} />;
27+
case DIALOG_FIELD_TYPES.radio:
28+
return <RadioField field={field} />;
29+
case DIALOG_FIELD_TYPES.tag:
30+
return <TagField field={field} />;
31+
case DIALOG_FIELD_TYPES.textBox:
32+
return <TextInputField field={field} />;
33+
case DIALOG_FIELD_TYPES.textArea:
34+
return <TextAreaField field={field} />;
35+
default:
36+
return <>{__('Field not supported')}</>;
37+
}
38+
};
39+
40+
/** Function to render a field. */
41+
const renderFieldItem = (field, data) => {
42+
const isRefreshing = data.fieldsToRefresh.includes(field.name);
43+
return (
44+
<div
45+
className={classNames('section-field-row', isRefreshing && 'field-refresh-in-progress')}
46+
key={field.id.toString()}
47+
id={`section-field-row-${field.name}`}
48+
>
49+
<div className="field-item">
50+
{
51+
renderFieldContent(field)
52+
}
53+
</div>
54+
<RefreshField field={field} />
55+
</div>
56+
);
57+
};
58+
59+
/** Component to render the Fields in the Service/DialogTabs/DialogGroups component */
60+
const DialogFields = ({ dialogFields }) => {
61+
const { data } = useContext(ServiceContext);
62+
return (
63+
<>
64+
{
65+
dialogFields.map((field) => (
66+
field.visible ? renderFieldItem(field, data) : <span key={field.id.toString()} />
67+
))
68+
}
69+
</>
70+
);
71+
};
72+
73+
DialogFields.propTypes = {
74+
dialogFields: PropTypes.arrayOf(PropTypes.any).isRequired,
75+
};
76+
77+
export default DialogFields;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import DialogFields from './DialogFields';
4+
5+
/** Component to render the Groups in the Service/DialogTabs component */
6+
const DialogGroups = ({ dialogGroups }) => (
7+
<>
8+
{
9+
dialogGroups.map((item) => (
10+
<div className="section" key={item.id.toString()}>
11+
<div className="section-label">
12+
{item.label}
13+
</div>
14+
<div className="section-fields">
15+
<DialogFields dialogFields={item.dialog_fields} />
16+
</div>
17+
</div>
18+
))
19+
}
20+
</>
21+
);
22+
23+
DialogGroups.propTypes = {
24+
dialogGroups: PropTypes.arrayOf(PropTypes.any).isRequired,
25+
};
26+
27+
export default DialogGroups;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React, { useContext } from 'react';
2+
import { Tabs, Tab } from 'carbon-components-react';
3+
import DialogGroups from './DialogGroups';
4+
import ServiceContext from './ServiceContext';
5+
import { extractDialogTabs } from './helper';
6+
7+
/** Component to render the Tabs in the Service component */
8+
const DialogTabs = () => {
9+
const { data } = useContext(ServiceContext);
10+
const dialogTabs = extractDialogTabs(data.apiResponse);
11+
return (
12+
<Tabs className="miq_custom_tabs">
13+
{
14+
dialogTabs.map((tab) => (
15+
<Tab key={tab.id.toString()} label={tab.label}>
16+
<div className="tabs">
17+
<DialogGroups dialogGroups={tab.dialog_groups} />
18+
</div>
19+
</Tab>
20+
))
21+
}
22+
</Tabs>
23+
);
24+
};
25+
26+
export default DialogTabs;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useContext } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Button, Loading } from 'carbon-components-react';
4+
import { Renew16 } from '@carbon/icons-react';
5+
import ServiceContext from './ServiceContext';
6+
import ServiceValidator from './ServiceValidator';
7+
import { defaultFieldValue, fieldProperties } from './helper';
8+
9+
/** Function to reset the dialogField data when the field refresh button is clicked. */
10+
const resetDialogField = (dialogFields, field) => {
11+
const { value, valid } = ServiceValidator.validateField({ field, value: defaultFieldValue(field) });
12+
dialogFields[field.name] = { value, valid };
13+
return { ...dialogFields };
14+
};
15+
16+
const RefreshField = ({ field }) => {
17+
const { data, setData } = useContext(ServiceContext);
18+
const { isDisabled } = fieldProperties(field, data);
19+
20+
const { fieldsToRefresh } = data;
21+
const inProgress = fieldsToRefresh.includes(field.name);
22+
return (
23+
<div className="refresh-field-item">
24+
{
25+
!!(field.dynamic && field.show_refresh_button) && !inProgress && (
26+
<Button
27+
hasIconOnly
28+
disabled={isDisabled}
29+
className="refresh-field-button"
30+
onClick={() => {
31+
setData({
32+
...data,
33+
fieldsToRefresh: [field.name],
34+
dialogFields: resetDialogField(data.dialogFields, field),
35+
});
36+
}}
37+
iconDescription={__(`Refresh ${field.label}`)}
38+
tooltipAlignment="start"
39+
tooltipPosition="left"
40+
renderIcon={Renew16}
41+
/>
42+
)
43+
}
44+
{
45+
inProgress && <Loading active small withOverlay={false} className="loading" />
46+
}
47+
</div>
48+
);
49+
};
50+
51+
RefreshField.propTypes = {
52+
field: PropTypes.shape({
53+
label: PropTypes.string,
54+
dynamic: PropTypes.bool,
55+
show_refresh_button: PropTypes.bool,
56+
dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
57+
name: PropTypes.string,
58+
}).isRequired,
59+
};
60+
61+
export default RefreshField;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useContext, useEffect } from 'react';
2+
import { Button } from 'carbon-components-react';
3+
import ServiceContext from './ServiceContext';
4+
import { omitValidation } from './helper';
5+
import miqRedirectBack from '../../helpers/miq-redirect-back';
6+
7+
const ServiceButtons = React.memo(() => {
8+
const { data, setData } = useContext(ServiceContext);
9+
const {
10+
apiAction, apiSubmitEndpoint, openUrl, finishSubmitEndpoint,
11+
} = data.urls;
12+
13+
useEffect(() => {
14+
if (data.locked) {
15+
const handleSubmission = async() => {
16+
const values = omitValidation(data.dialogFields);
17+
let submitData = { action: 'order', ...values };
18+
19+
if (apiSubmitEndpoint.includes('/generic_objects/')) {
20+
submitData = { action: apiAction, parameters: values };
21+
} else if (apiAction === 'reconfigure') {
22+
submitData = { action: apiAction, resource: values };
23+
}
24+
25+
try {
26+
const response = await API.post(apiSubmitEndpoint, submitData, { skipErrors: [400] });
27+
if (openUrl === 'true') {
28+
const taskResponse = await API.wait_for_task(response)
29+
.then(() =>
30+
// eslint-disable-next-line no-undef
31+
$http.post('open_url_after_dialog', { targetId, realTargetType }));
32+
33+
if (taskResponse.data.open_url) {
34+
window.open(response.data.open_url);
35+
miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint);
36+
} else {
37+
add_flash(__('Automate failed to obtain URL.'), 'error');
38+
miqSparkleOff();
39+
}
40+
} else {
41+
miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint);
42+
}
43+
} catch (_error) {
44+
// Handle error if needed
45+
}
46+
};
47+
48+
handleSubmission();
49+
}
50+
}, [data.locked]);
51+
52+
const formValid = data.dialogFields ? Object.values(data.dialogFields).every((field) => field.valid) : false;
53+
54+
const submitForm = () => {
55+
miqSparkleOn();
56+
setData({
57+
...data,
58+
locked: true,
59+
});
60+
};
61+
62+
return (
63+
<div className="service-action-buttons">
64+
<Button
65+
disabled={!formValid || data.locked}
66+
onClick={submitForm}
67+
>
68+
{
69+
data.locked ? __('Submitting...') : __('Submit')
70+
}
71+
</Button>
72+
73+
<Button
74+
kind="secondary"
75+
disabled={data.locked}
76+
onClick={() => miqRedirectBack(__('Dialog Cancelled'), 'warning', '/catalog')}
77+
>
78+
{__('Cancel')}
79+
</Button>
80+
</div>
81+
);
82+
});
83+
84+
export default ServiceButtons;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createContext } from 'react';
2+
3+
const ServiceContext = createContext();
4+
export default ServiceContext;

0 commit comments

Comments
 (0)