From 6e74bc871872762664d7e74600fd9049d555a085 Mon Sep 17 00:00:00 2001 From: Gilbert Cherrie Date: Wed, 7 Sep 2022 14:27:06 -0400 Subject: [PATCH] Convert order service form --- app/controllers/catalog_controller.rb | 1 + app/helpers/catalog_helper.rb | 1 + app/helpers/order_service_helper.rb | 17 + .../order-service-form/fields.schema.js | 249 ++++++++++++ .../components/order-service-form/helper.js | 321 ++++++++++++++++ .../components/order-service-form/index.jsx | 303 +++++++++++++++ .../order-service-constants.js | 18 + .../order-service-form.schema.js | 34 ++ .../order-service-refresh-button.jsx | 49 +++ .../components/service-dialog/helper.js | 118 ++++++ .../components/service-dialog/index.jsx | 81 ++++ .../service-dialog/service-dialog.js | 13 + .../service-dialog/viewFields.schema.js | 204 ++++++++++ .../actions/order-service-actions.js | 19 + .../reducers/order-service-reducer.js | 26 ++ app/javascript/miq-redux/store.js | 2 + .../dialog_user/dialog_user_controller.js | 3 +- .../packs/component-definitions-common.js | 9 +- .../__snapshots__/action-form.spec.js.snap | 1 + ...d-remove-security-groups-form.spec.js.snap | 4 + .../ansible-credentials-form.spec.js.snap | 2 + .../ansible-edit-catalog-form.spec.js.snap | 3 + .../button-group-form.spec.js.snap | 2 + .../c-and-u-collections-form.spec.js.snap | 2 + .../cloud-database-form.spec.js.snap | 1 + ...d-object-store-container-form.spec.js.snap | 3 + .../cloud-tenant-form.spec.js.snap | 1 + .../cloud-volume-actions-form.spec.js.snap | 6 + .../cloud-volume-backup-form.spec.js.snap | 2 + ...tach-detach-cloud-volume-form.spec.js.snap | 6 + .../custom-button-form.spec.js.snap | 2 + .../__snapshots__/datastore-form.spec.js.snap | 2 + .../diagnostics-collect-log-form.spec.js.snap | 3 + .../__snapshots__/evacuate-form.spec.js.snap | 3 + .../filter-dropdown.spec.js.snap | 1 + .../generic-objects-form.spec.js.snap | 3 + .../host-aggregate-form.spec.js.snap | 2 + .../__snapshots__/host-edit-form.spec.js.snap | 2 + .../host-initiator-group.spec.js.snap | 1 + .../live-migrate-form.spec.js.snap | 3 + .../order-service-form.spec.js.snap | 359 ++++++++++++++++++ .../order-service-form.spec.js | 304 +++++++++++++++ .../physical-storage-form.spec.js.snap | 1 + .../policy-profile-form.spec.js.snap | 2 + ...e-customization-template-form.spec.js.snap | 3 + .../pxe-image-type-form.spec.js.snap | 2 + .../pxe-iso-datastore-form.spec.js.snap | 1 + .../pxe-iso-image-form.spec.js.snap | 1 + .../reconfigure-vm-form.spec.js.snap | 9 + .../retirement-form.spec.js.snap | 2 + .../__snapshots__/schedule-form.spec.js.snap | 3 + .../service-request-default-form.spec.js.snap | 1 + .../settings-category-form.spec.js.snap | 2 + .../settings-time-profile-form.spec.js.snap | 5 + .../__snapshots__/subnet-form.spec.js.snap | 1 + .../tenant-quota-form.spec.js.snap | 2 + .../__snapshots__/timeline-chart.spec.js.snap | 1 + .../vm-common-rename-form.spec.js.snap | 1 + .../__snapshots__/vm-edit-form.spec.js.snap | 5 + .../vm-floating-ips-form.spec.js.snap | 4 + .../__snapshots__/vm-resize-form.spec.js.snap | 2 + .../__snapshots__/widget-wrapper.spec.js.snap | 3 + ...kflow-credential-mapping-form.spec.js.snap | 2 + .../workflow-credentials-form.spec.js.snap | 2 + .../__snapshots__/zone-form.spec.js.snap | 1 + app/stylesheet/ddf_override.scss | 44 ++- .../_dialog_sample.html.haml | 107 +----- app/views/miq_request/_st_prov_show.html.haml | 4 + .../shared/dialogs/_dialog_user.html.haml | 16 + 69 files changed, 2303 insertions(+), 110 deletions(-) create mode 100644 app/helpers/order_service_helper.rb create mode 100644 app/javascript/components/order-service-form/fields.schema.js create mode 100644 app/javascript/components/order-service-form/helper.js create mode 100644 app/javascript/components/order-service-form/index.jsx create mode 100644 app/javascript/components/order-service-form/order-service-constants.js create mode 100644 app/javascript/components/order-service-form/order-service-form.schema.js create mode 100644 app/javascript/components/order-service-form/order-service-refresh-button.jsx create mode 100644 app/javascript/components/service-dialog/helper.js create mode 100644 app/javascript/components/service-dialog/index.jsx create mode 100644 app/javascript/components/service-dialog/service-dialog.js create mode 100644 app/javascript/components/service-dialog/viewFields.schema.js create mode 100644 app/javascript/miq-redux/actions/order-service-actions.js create mode 100644 app/javascript/miq-redux/reducers/order-service-reducer.js create mode 100644 app/javascript/spec/order-service-form/__snapshots__/order-service-form.spec.js.snap create mode 100644 app/javascript/spec/order-service-form/order-service-form.spec.js diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index e654f8b4503a..ecaa1ebe2453 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -739,6 +739,7 @@ def svc_catalog_provision ra, st, svc_catalog_provision_finish_submit_endpoint ) @in_a_form = true + @dialog_locals = options[:dialog_locals] replace_right_cell(:action => "dialog_provision", :dialog_locals => options[:dialog_locals]) else # if catalog item has no dialog and provision button was pressed from list view diff --git a/app/helpers/catalog_helper.rb b/app/helpers/catalog_helper.rb index 0fdeaecfb943..0d8219d2f24e 100644 --- a/app/helpers/catalog_helper.rb +++ b/app/helpers/catalog_helper.rb @@ -2,6 +2,7 @@ module CatalogHelper include_concern 'TextualSummary' include RequestInfoHelper include Mixins::AutomationMixin + include OrderServiceHelper def miq_catalog_resource(resources) headers = ["", _("Name"), _("Description"), _("Action Order"), _("Provision Order"), _("Action Start"), _("Action Stop"), _("Delay (mins) Start"), _("Delay (mins) Stop")] diff --git a/app/helpers/order_service_helper.rb b/app/helpers/order_service_helper.rb new file mode 100644 index 000000000000..fae935cb9cc7 --- /dev/null +++ b/app/helpers/order_service_helper.rb @@ -0,0 +1,17 @@ +module OrderServiceHelper + def order_service_data(dialog) + { + + :apiSubmitEndpoint => dialog[:api_submit_endpoint], + :apiAction => dialog[:api_action], + :cancelEndPoint => dialog[:cancel_endpoint], + :dialogId => dialog[:dialog_id], + :finishSubmitEndpoint => dialog[:finish_submit_endpoint], + :openUrl => dialog[:open_url], + :resourceActionId => dialog[:resource_action_id], + :realTargetType => dialog[:real_target_type], + :targetId => dialog[:target_id], + :targetType => dialog[:target_type], + } + end +end diff --git a/app/javascript/components/order-service-form/fields.schema.js b/app/javascript/components/order-service-form/fields.schema.js new file mode 100644 index 000000000000..e1cf0f6e07fc --- /dev/null +++ b/app/javascript/components/order-service-form/fields.schema.js @@ -0,0 +1,249 @@ +import { componentTypes } from '@@ddf'; +import { REFERENCE_TYPES } from './order-service-constants'; +// eslint-disable-next-line import/no-cycle + +/** Function to build a text box. */ +export const buildTextBox = (field, validate, updateFormReference) => { + let component = {}; + + if (field.options.protected) { + component = { + component: 'password-field', + id: field.id, + name: field.name, + label: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + }; + } else { + component = { + component: componentTypes.TEXT_FIELD, + id: field.id, + name: field.name, + label: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, + }; + } + return component; +}; + +/** Function to build a text area */ +export const buildTextAreaBox = (field, validate, updateFormReference) => ({ + component: componentTypes.TEXTAREA, + id: field.id, + name: field.name, + label: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, +}); + +/** Function to build a check box. */ +export const buildCheckBox = (field, validate, updateFormReference) => ({ + component: componentTypes.CHECKBOX, + id: field.id, + name: field.name, + label: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, +}); + +/** Function to build a tag control field. */ +export const buildTagControl = (field, validate, updateFormReference) => { + const options = []; + field.values.forEach((value) => { + if (!value.id) { + value.id = '-1'; + } + options.push({ value: value.id, label: value.description }); + }); + return { + component: componentTypes.SELECT, + id: field.id, + name: field.name, + label: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + options, + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, + }; +}; + +/** Function to build a date control field */ +export const buildDateControl = (field, validate, updateFormReference) => ({ + component: componentTypes.DATE_PICKER, + id: field.id, + name: field.name, + label: field.label, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + variant: 'date-time', + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, +}); + +/** Function to build a time control field */ +export const buildTimeControl = (field, validate, updateFormReference, dateTime) => ([{ + component: componentTypes.DATE_PICKER, + id: field.id, + name: field.name, + label: field.label, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: dateTime.toISOString(), + description: field.description, + validate, + variant: 'date-time', + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, +}, +{ + component: componentTypes.TIME_PICKER, + id: `${field.id}-time`, + name: `${field.name}-time`, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: dateTime, + validate, + twelveHoursFormat: true, + pattern: '(0?[1-9]|1[0-2]):[0-5][0-9]', + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, +}]); + +/** Function to build radio buttons fields */ +export const buildRadioButtons = (field, validate, updateFormReference) => { + const options = []; + field.values.forEach((value) => { + options.push({ value: value[0], label: value[1] }); + }); + return { + component: componentTypes.RADIO, + id: field.id, + name: field.name, + label: field.label, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + options, + resolveProps: (props, { input }) => { + updateFormReference({ type: REFERENCE_TYPES.dialogFields, payload: { fieldName: input.name, value: input.value } }); + }, + }; +}; + +/** Function to build a refresh button near to drop down. */ +export const buildRefreshButton = (field, tabIndex, { updateFormReference }) => ({ + component: 'refresh-button', + name: `refresh_${field.name}`, + data: { + showRefreshButton: !!(field.dynamic && field.show_refresh_button), + fieldName: field.name, + tabIndex, + disabled: false, + updateRefreshInProgress: (status) => updateFormReference({ type: REFERENCE_TYPES.refreshInProgress, payload: status }), + }, +}); + +const buildOptions = (field) => { + let options = []; + let placeholder = __(''); + let start; + + field.values.forEach((value) => { + if (value[0] === null) { + value[0] = null; + // eslint-disable-next-line prefer-destructuring + placeholder = value[1]; + } + options.push({ value: value[0] !== null ? String(value[0]) : null, label: value[1] }); + }); + if (options[0].value === null) { + start = options.shift(); + } + options = options.sort((option1, option2) => { + if (field.options.sort_by === 'description') { + if (field.options.sort_order === 'ascending') { + return option1.label.localeCompare(option2.label); + } + return option2.label.localeCompare(option1.label); + } + if (field.options.sort_order === 'ascending') { + return option1.value.localeCompare(option2.value); + } + return option2.value.localeCompare(option1.value); + }); + if (start) { + options.unshift(start); + } + return { options, placeholder }; +}; + +/** Function to build a drop down select box. */ +export const buildDropDownList = (field, validate, fieldOnChange, _responseFrom) => { + const { options, placeholder } = buildOptions(field); + + let isMulti = false; + if (field.options && field.options.force_multi_value) { + isMulti = true; + } + return { + component: componentTypes.SELECT, + id: field.id, + name: field.name, + labelText: field.label, + hideField: !field.visible, + isRequired: field.required, + isDisabled: field.read_only, + initialValue: field.default_value, + description: field.description, + validate, + options, + placeholder, + isSearchable: true, + simpleValue: true, + isMulti, + onChange: (value) => { + fieldOnChange(value, field); + }, + }; +}; diff --git a/app/javascript/components/order-service-form/helper.js b/app/javascript/components/order-service-form/helper.js new file mode 100644 index 000000000000..2f9453656dd8 --- /dev/null +++ b/app/javascript/components/order-service-form/helper.js @@ -0,0 +1,321 @@ +import { componentTypes, validatorTypes } from '@@ddf'; +import { DIALOG_FIELDS, REFERENCE_TYPES } from './order-service-constants'; +// eslint-disable-next-line import/no-cycle +import { + buildTextBox, + buildTextAreaBox, + buildCheckBox, + buildDropDownList, + buildTagControl, + buildDateControl, + buildTimeControl, + buildRadioButtons, + buildRefreshButton, +} from './fields.schema'; + +const dates = []; +const showPastDates = []; +const showPastDatesFieldErrors = []; +const checkBoxes = []; +let hasTime = false; +let stopSubmit = false; +let invalidDateFields = []; + +const formatDateControl = (field) => { + dates.push(field.name); + if (field.default_value === '' || !field.default_value) { + const today = new Date(); + field.default_value = today.toISOString(); + } + if (field.options.show_past_dates) { + showPastDates.push(field.name); + } else { + showPastDatesFieldErrors.push({ name: field.name, label: field.label }); + } +}; + +const formatTimeControl = (field) => { + let newDate = ''; + hasTime = true; + if (field.default_value === '' || !field.default_value) { + newDate = new Date(); + field.default_value = newDate.toISOString(); + } else { + newDate = new Date(field.default_value); + } + if (field.options.show_past_dates) { + showPastDates.push(field.name); + } else { + showPastDatesFieldErrors.push({ name: field.name, label: field.label }); + } + return newDate; +}; + +const buildComponent = (field, validate, { updateFormReference, fieldOnChange }, responseFrom) => { + switch (field.type) { + case DIALOG_FIELDS.textBox: + return buildTextBox(field, validate, updateFormReference); + case DIALOG_FIELDS.textArea: + return buildTextAreaBox(field, validate, updateFormReference); + case DIALOG_FIELDS.checkBox: + return buildCheckBox(field, validate, updateFormReference); + case DIALOG_FIELDS.dropDown: + return buildDropDownList(field, validate, fieldOnChange, responseFrom); + case DIALOG_FIELDS.tag: + return buildTagControl(field, validate, updateFormReference); + case DIALOG_FIELDS.date: + { + formatDateControl(field); + return buildDateControl(field, validate, updateFormReference); + } + case DIALOG_FIELDS.dateTime: + { + const dateTime = formatTimeControl(field); + return buildTimeControl(field, validate, updateFormReference, dateTime); + } + case DIALOG_FIELDS.radio: + return buildRadioButtons(field, validate, updateFormReference); + default: + return {}; + } +}; + +/** Function to build a field inside a section. */ +const sectionField = (group, componentItem, index) => ({ + component: componentTypes.SUB_FORM, + id: `${group.id.toString()}_row`, + name: group.label, + fields: componentItem, + className: 'order-form-row', + key: index, +}); + +/** Function to build the validators of a field. */ +const buildValidator = (field) => { + const validate = []; + if (field.validator_rule) { + if (field.validator_message) { + validate.push({ + type: validatorTypes.PATTERN, + pattern: field.validator_rule, + message: field.validator_message, + }); + } else { + validate.push({ + type: validatorTypes.PATTERN, + pattern: field.validator_rule, + }); + } + } + if (field.required) { + validate.push({ + type: validatorTypes.REQUIRED, + }); + } +}; + +/** Function to build the form fields. */ +export const buildFields = (response, data, setData, orderServiceConfig, responseFrom) => { + const newResponse = { ...response }; + const dialogTabs = []; + response.content[0].dialog_tabs.forEach((tab, tabIndex) => { + const dialogSections = []; + tab.dialog_groups.forEach((group, _groupIndex) => { + const dialogFields = []; + group.dialog_fields.forEach((field) => { + const validate = buildValidator(field); + orderServiceConfig.updateFormReference({ + type: REFERENCE_TYPES.dialogFields, + payload: { fieldName: field.name, value: field.default_value } + }); + orderServiceConfig.updateFormReference({ + type: REFERENCE_TYPES.fieldResponders, + payload: { fieldName: field.name, responders: field.dialog_field_responders }, + }); + const fieldData = [ + buildComponent(field, validate, orderServiceConfig, responseFrom), + buildRefreshButton(field, tabIndex, orderServiceConfig), + ]; + dialogFields.push(fieldData); + }); + + const sectionData = { + component: componentTypes.SUB_FORM, + id: group.id, + name: group.label, + title: group.label, + fields: dialogFields.map((item, index) => sectionField(group, item, index)), + }; + dialogSections.push(sectionData); + }); + + const tabData = { + name: tab.label, + title: tab.label, + fields: dialogSections, + onClick: () => orderServiceConfig.updateFormReference({ type: REFERENCE_TYPES.activeTab, payload: tabIndex }), + }; + dialogTabs.push(tabData); + }); + setData({ + ...data, + response: newResponse, + responseFrom, // useEffect action is declared in 'OrderServiceForm'. + fields: dialogTabs, + isLoading: false, + hasTime, + showPastDates, + showPastDatesFieldErrors, + checkBoxes, + dates, + }); +}; + +/** Function to reformat the dates. */ +const datePassed = (selectedDate) => { + const userDate = new Date(selectedDate); + const today = new Date(); + + if (userDate.getDate() === today.getDate() && userDate.getMonth() === today.getMonth() && userDate.getFullYear() === today.getFullYear()) { + return false; + } + + if (userDate < today) { + return true; + } + return false; +}; + +/** Function to handle the time picker format on submit. */ +const handleTimePickerSubmit = (submitData) => { + let tempSubmitData; + // Loop through fields to check for time fields + Object.entries(submitData).forEach((tempField) => { + let fieldName = `${tempField[0]}`; + let fieldValue = ''; + if (fieldName.includes('-time')) { + fieldName = fieldName.substring(0, fieldName.length - 5); + // eslint-disable-next-line prefer-destructuring + fieldValue = tempField[1]; + // If time field found loop through fields again to find corresponding date field + Object.entries(submitData).forEach((field) => { + if (field[0] === fieldName) { + const timeValue = new Date(fieldValue); + const dateValue = new Date(field[1]); + const newDate = new Date(dateValue.setHours(timeValue.getHours(), timeValue.getMinutes())); + submitData[field[0]] = newDate.toISOString(); // Set new date and time + + // Check for fields that don't allow previous dates + if (!showPastDates.includes(fieldName) && datePassed(newDate)) { + stopSubmit = true; + // Loop through all fields that don't allow previous dates + showPastDatesFieldErrors.forEach((dateField) => { + // Check if current field is found in the list of fields that don't allow previous dates + if (fieldName === dateField.name) { + // Add field label to list of invalid date fields + invalidDateFields.push(dateField.label); + } + }); + } + } + }); + tempSubmitData = _.omit(submitData, tempField[0]); + } + }); + return tempSubmitData; +}; + +/** Function to handle the date picker format on submit. */ +const handleDatePickerSubmit = (submitData) => { + Object.entries(submitData).forEach((field) => { + const fieldName = field[0]; + const fieldValue = field[1]; + dates.forEach((date) => { + if (date === fieldName) { + if (Array.isArray(fieldValue)) { + // eslint-disable-next-line prefer-destructuring + submitData[fieldName] = fieldValue[0]; + } else { + submitData[fieldName] = fieldValue; + } + const dateValue = new Date(submitData[fieldName]); + submitData[fieldValue] = dateValue.toISOString(); // Set new date and time + + if (!showPastDates.includes(fieldName) && datePassed(dateValue)) { + stopSubmit = true; + // Loop through all fields that don't allow previous dates + showPastDatesFieldErrors.forEach((dateField) => { + // Check if current field is found in the list of fields that don't allow previous dates + if (fieldName === dateField.name) { + // Add field label to list of invalid date fields + invalidDateFields.push(dateField.label); + } + }); + } + } + }); + }); +}; + +/** Function to handle the checkbox data format on submit. */ +const handleCheckboxSubmit = (submitData) => { + Object.entries(submitData).forEach((field) => { + const fieldName = field[0]; + const fieldValue = field[1]; + checkBoxes.forEach((checkbox) => { + if (checkbox === fieldName) { + if (fieldValue) { + submitData[fieldName] = 't'; + } else { + submitData[fieldName] = 'f'; + } + } + }); + }); +}; + +/** Function to handle the form data on form submit. */ +export const prepareSubmitData = (values, setShowDateError) => { + let submitData = { action: 'order', ...values }; + stopSubmit = false; + invalidDateFields = []; + + if (hasTime) { + submitData = handleTimePickerSubmit(submitData); + } + + if (dates.length > 0) { + handleDatePickerSubmit(submitData); + } + + setShowDateError(invalidDateFields); + + if (checkBoxes.length > 0) { + handleCheckboxSubmit(submitData); + } + + if (stopSubmit) { + return false; + } + return submitData; +}; + +/** Function to update the response and build the fileds again after field refresh. */ +export const updateResponseFieldsData = (response, currentRefreshField, result) => { + const data = result[currentRefreshField]; + response.content[0].dialog_tabs.map((tab) => tab.dialog_groups.map((group) => group.dialog_fields.map((field) => { + if (field.name === currentRefreshField) { + field.data_type = data.data_type; + field.options = data.options; + field.read_only = data.read_only; + field.required = data.required; + field.visible = data.visible; + field.values = data.values; + field.default_value = data.default_value; + field.validator_rule = data.validator_rule; + field.validator_type = data.validator_type; + } + return field; + }))); + return { ...response }; +}; diff --git a/app/javascript/components/order-service-form/index.jsx b/app/javascript/components/order-service-form/index.jsx new file mode 100644 index 000000000000..b29a4795c86c --- /dev/null +++ b/app/javascript/components/order-service-form/index.jsx @@ -0,0 +1,303 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import MiqFormRenderer, { useFormApi } from '@@ddf'; +import { Button, Loading } from 'carbon-components-react'; +import { FormSpy } from '@data-driven-forms/react-form-renderer'; +import createSchema from './order-service-form.schema'; +import { API } from '../../http_api'; +import miqRedirectBack from '../../helpers/miq-redirect-back'; +import { + buildFields, prepareSubmitData, updateResponseFieldsData, +} from './helper'; +import OrderServiceRefreshButton from './order-service-refresh-button'; +import mapper from '../../forms/mappers/componentMapper'; +import NotificationMessage from '../notification-message'; +import { orderServiceActions } from '../../miq-redux/actions/order-service-actions'; +import { REFERENCE_TYPES } from './order-service-constants'; + +const OrderServiceForm = ({ initialData }) => { + const orderServiceReducer = useSelector((state) => state.orderService); + const dispatch = useDispatch(); + + const { + dialogId, resourceActionId, targetId, targetType, apiSubmitEndpoint, apiAction, openUrl, realTargetType, finishSubmitEndpoint, + } = initialData; + + const componentMapper = { + ...mapper, + 'refresh-button': OrderServiceRefreshButton, + }; + + const resource = { + resource_action_id: initialData.resourceActionId, + target_id: initialData.targetId, + target_type: initialData.targetType, + real_target_type: initialData.realTargetType, + }; + + const [data, setData] = useState({ + isLoading: true, + response: undefined, + responseFrom: 'pageLoad', + fields: [], + hasTime: false, + showPastDates: [], + showPastDatesFieldErrors: [], + dateErrorFields: [], + checkBoxes: [], + dates: [], + notification: undefined, + }); + const [showDateError, setShowDateError] = useState([]); + + /** 'dialogFields' are used to store the dynamic_field values that needs to be passed into the API. */ + const dialogFields = useRef({}); + + /** fieldsToRefresh is an array of field names that has to refreshed. */ + const fieldsToRefresh = useRef([]); + + /** 'refreshInProgress' will be true when a field's refresh action is in progress. */ + const refreshInProgress = useRef(false); + + /** 'fieldResponders' holds the field and its dialog_field_responders received from the api response. */ + const fieldResponders = useRef({}); + + const updateActiveTab = (activeTab) => { dispatch(orderServiceActions.setActiveTab({ activeTab })); }; + const updateDialogFields = ({ fieldName, value }) => { dialogFields.current = { ...dialogFields.current, [fieldName]: value }; }; + const updateFieldsToRefresh = (fieldNames) => { fieldsToRefresh.current = fieldNames; }; + const updateFieldResponders = ({ fieldName, responders }) => { fieldResponders.current = { ...fieldResponders.current, [fieldName]: responders }; }; + const updateRefreshInProgress = (status) => { refreshInProgress.current = status; }; + + /** Function to update the 'activeTab' in redux. This is the selected tab's index. + * This is used to display the selected tab when the 'refreshField' function is in progress. + * For Eg: if a refresh action is in progress in tab 1 and we want to see whats happening on tab 3. + */ + const changeCurrentRefreshField = (responders) => { + let nextRefreshField; + if (responders.length > 0) { + nextRefreshField = responders.shift(); // Select the first item from the array. + updateFieldsToRefresh([...responders]); + } else { + updateRefreshInProgress(false); + } + dispatch(orderServiceActions.setCurrentRefreshField({ currentRefreshField: nextRefreshField })); + }; + + const fieldOnChange = (value, field) => { + if (refreshInProgress.current === false && dialogFields.current[field.name] !== value) { + updateDialogFields({ fieldName: field.name, value }); + const responders = fieldResponders.current[field.name]; + if (responders.length > 0) { + changeCurrentRefreshField(responders); + } + } + }; + + const updateFormReference = ({ type, payload }) => { + switch (type) { + case REFERENCE_TYPES.activeTab: + return updateActiveTab(payload); + case REFERENCE_TYPES.dialogFields: + return updateDialogFields(payload); + case REFERENCE_TYPES.fieldsToRefresh: + return updateFieldsToRefresh(payload); + case REFERENCE_TYPES.fieldResponders: + return updateFieldResponders(payload); + case REFERENCE_TYPES.refreshInProgress: + return updateRefreshInProgress(payload); + default: + return undefined; + } + }; + + /** 'orderServiceConfig' is used a reference to pass the react specific function to helper files. */ + const orderServiceConfig = { + fieldOnChange, + updateFormReference, + }; + + /** Function handle the refresh button's click event initiated from the 'OrderServiceRefreshButton' component. + * When the api response is received, it updates the 'data.response' with the new response. + * The dialog_field_responders are added to fieldsToRefresh's array. + */ + const refreshField = () => { + const { currentRefreshField } = orderServiceReducer; + if (currentRefreshField) { + const params = { + action: 'refresh_dialog_fields', + resource: { + ...resource, + dialog_fields: dialogFields.current, + fields: [currentRefreshField], + }, + }; + API.post(`/api/service_dialogs/${data.response.id}`, params).then(({ result }) => { + const newResponse = updateResponseFieldsData(data.response, currentRefreshField, result); + const newFieldsToRefresh = result[currentRefreshField].dialog_field_responders; + updateFieldsToRefresh([...fieldsToRefresh.current, ...newFieldsToRefresh]); + buildFields(newResponse, { ...data }, setData, orderServiceConfig, currentRefreshField); + }); + } + }; + + /** This works when the value in data.response is changed. + * The first item from 'fieldsToRefresh' array is selected to be refreshed and removed from the array. */ + useEffect(() => { + if (data.responseFrom !== 'pageLoad') { + changeCurrentRefreshField(fieldsToRefresh.current); + } + }, [data.responseFrom]); + + /** The 'currentRefreshField' is stored in redux. + * When this value is changed, the 'refreshField' action will be executed. + * This can be changed from: + * - 'OrderServiceRefreshButton' component's refresh button click event. + * - After when a field is refreshed, the 'data.responseFrom' is updated which has a useEffect defined. + * - This works like a cycle until there is nothing to refresh. */ + useEffect(() => { + const { currentRefreshField } = orderServiceReducer; + if (currentRefreshField) { + refreshField(); + } + }, [orderServiceReducer.currentRefreshField]); + + /** This useEffect is executed when the form is first loaded. */ + useEffect(() => { + const url = `/api/service_dialogs/${dialogId}?resource_action_id=${resourceActionId}&target_id=${targetId}&target_type=${targetType}`; + API.get(url, { skipErrors: [500] }) + .then((response) => buildFields(response, data, setData, orderServiceConfig, 'pageLoad')) + .catch((errorData) => { + const message = (typeof (errorData) === 'string') ? errorData : errorData.data.error; + setData({ + ...data, + isLoading: false, + notification: { type: 'error', message }, + }); + }); + }, []); + + const onSubmit = (values) => { + let submitData = { action: 'order', ...values }; + + submitData = prepareSubmitData(values, setShowDateError); + + if (submitData !== false) { + if (apiSubmitEndpoint.includes('/generic_objects/')) { + submitData = { action: apiAction, parameters: _.omit(submitData, 'action') }; + } else if (apiAction === 'reconfigure') { + submitData = { action: apiAction, resource: _.omit(submitData, 'action') }; + } + return API.post(apiSubmitEndpoint, submitData, { skipErrors: [400] }) + .then((response) => { + if (openUrl === 'true') { + return API.wait_for_task(response) + .then(() => + // eslint-disable-next-line no-undef + $http.post('open_url_after_dialog', { targetId, realTargetType })) + .then((taskResponse) => { + if (taskResponse.data.open_url) { + window.open(response.data.open_url); + miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint); + } else { + add_flash(__('Automate failed to obtain URL.'), 'error'); + miqSparkleOff(); + } + }); + } + miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint); + return null; + }); + } + return null; + }; + + const onCancel = () => miqRedirectBack(__('Dialog Cancelled'), 'warning', '/catalog'); + + return ( + <> + { + data.notification && + } + { + data.isLoading && ( +
+ +
+ ) + } + { + !data.isLoading && ( + } + componentMapper={componentMapper} + schema={createSchema(data.fields, showDateError, orderServiceReducer.activeTab)} + initialValues={data.initialValues} + onSubmit={onSubmit} + onCancel={onCancel} + /> + ) + } + + ); +}; + +const verifyIsDisabled = (valid) => { + let isDisabled = true; + if (valid) { + isDisabled = false; + } + return isDisabled; +}; + +const FormTemplate = ({ formFields }) => { + const { + handleSubmit, onCancel, getState, + } = useFormApi(); + const { values, valid } = getState(); + return ( +
+ {formFields} + + {() => ( +
+ + +
+ )} +
+
+ ); +}; + +FormTemplate.propTypes = { + formFields: PropTypes.arrayOf(PropTypes.any).isRequired, +}; + +OrderServiceForm.propTypes = { + initialData: PropTypes.shape({ + apiSubmitEndpoint: PropTypes.string.isRequired, + apiAction: PropTypes.string.isRequired, + cancelEndPoint: PropTypes.string.isRequired, + dialogId: PropTypes.number.isRequired, + finishSubmitEndpoint: PropTypes.string.isRequired, + openUrl: PropTypes.bool.isRequired, + resourceActionId: PropTypes.number.isRequired, + realTargetType: PropTypes.string.isRequired, + targetId: PropTypes.number.isRequired, + targetType: PropTypes.string.isRequired, + }).isRequired, +}; + +export default OrderServiceForm; diff --git a/app/javascript/components/order-service-form/order-service-constants.js b/app/javascript/components/order-service-form/order-service-constants.js new file mode 100644 index 000000000000..5a10e4856e02 --- /dev/null +++ b/app/javascript/components/order-service-form/order-service-constants.js @@ -0,0 +1,18 @@ +export const DIALOG_FIELDS = { + checkBox: 'DialogFieldCheckBox', + date: 'DialogFieldDateControl', + dateTime: 'DialogFieldDateTimeControl', + dropDown: 'DialogFieldDropDownList', + radio: 'DialogFieldRadioButton', + tag: 'DialogFieldTagControl', + textBox: 'DialogFieldTextBox', + textArea: 'DialogFieldTextAreaBox', +}; + +export const REFERENCE_TYPES = { + activeTab: 'ACTIVE-TAB', + dialogFields: 'DIALOG-FIELDS', + fieldsToRefresh: 'FIELDS-TO-REFRESH', + fieldResponders: 'FIELD-RESPONDERS', + refreshInProgress: 'REFRESH-IN-PROGRESS', +}; diff --git a/app/javascript/components/order-service-form/order-service-form.schema.js b/app/javascript/components/order-service-form/order-service-form.schema.js new file mode 100644 index 000000000000..2da6bf50b67f --- /dev/null +++ b/app/javascript/components/order-service-form/order-service-form.schema.js @@ -0,0 +1,34 @@ +import { componentTypes } from '@@ddf'; + +const showDateErrorFields = (fields) => { + let invalidFields; + fields.forEach((field) => { + if (invalidFields) { + invalidFields = `${invalidFields}, ${field}`; + } else { + invalidFields = field; + } + }); + return invalidFields; +}; + +const createSchema = (fields, showDateError, activeTab) => ({ + fields: [ + { + component: componentTypes.TABS, + name: 'tabs', + fields, + selected: activeTab, + }, + ...(showDateError.length > 0 ? [ + { + id: 'dateWarning', + component: componentTypes.PLAIN_TEXT, + name: 'dateWarning', + label: __(`Invalid date selected for ${showDateErrorFields(showDateError)}. Please select a future date.`), + }, + ] : []), + ], +}); + +export default createSchema; diff --git a/app/javascript/components/order-service-form/order-service-refresh-button.jsx b/app/javascript/components/order-service-form/order-service-refresh-button.jsx new file mode 100644 index 000000000000..8f73ae9fce54 --- /dev/null +++ b/app/javascript/components/order-service-form/order-service-refresh-button.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Button, Loading } from 'carbon-components-react'; +import { Renew16 } from '@carbon/icons-react'; +import PropTypes from 'prop-types'; +import { useSelector, useDispatch } from 'react-redux'; +import { orderServiceActions } from '../../miq-redux/actions/order-service-actions'; + +const OrderServiceRefreshButton = ({ data }) => { + const dispatch = useDispatch(); + const orderServiceReducer = useSelector((state) => state.orderService); + + return ( + <> + {data.showRefreshButton && ( +