diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb
index 6540d534df1..3e41e5b28e7 100644
--- a/app/controllers/catalog_controller.rb
+++ b/app/controllers/catalog_controller.rb
@@ -743,6 +743,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 6d665742e56..b0fbbfd99b6 100644
--- a/app/helpers/catalog_helper.rb
+++ b/app/helpers/catalog_helper.rb
@@ -3,6 +3,7 @@ module CatalogHelper
include RequestInfoHelper
include Mixins::AutomationMixin
include OrchestrationTemplateHelper
+ 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/miq_request_helper.rb b/app/helpers/miq_request_helper.rb
index 418ebf678fc..93366d994bb 100644
--- a/app/helpers/miq_request_helper.rb
+++ b/app/helpers/miq_request_helper.rb
@@ -1,12 +1,54 @@
+require 'byebug'
module MiqRequestHelper
include RequestInfoHelper
include RequestDetailsHelper
+ include OrderServiceHelper
def row_data(label, value)
{:cells => {:label => label, :value => value}}
end
def request_task_configuration_script_ids(miq_request)
- miq_request.miq_request_tasks.map { |task| task.options&.dig(:configuration_script_id) }.compact
+ miq_request.miq_request_tasks.filter_map { |task| task.options&.dig(:configuration_script_id) }
+ end
+
+ def select_box_options(options)
+ options.map do |item|
+ if /Classification::(\d+)/.match?(item)
+ classification_id = item.match(/Classification::(\d+)/)[1]
+ classification = Classification.find_by(:id => classification_id)
+ if classification
+ {:label => classification.description, :value => classification.id.to_s}
+ end
+ else
+ classification = Classification.find_by(:id => item)
+ if classification
+ {:label => classification.description, :value => classification.id.to_s}
+ end
+ end
+ end
+ end
+
+ def dialog_field_values(dialog, wf)
+
+ transformed_data = dialog.transform_keys { |key| key.sub('Array::', '') }
+ transformed_data.transform_values do |value|
+ if value.to_s.include?("\u001F")
+ select_box_options(value.split("\u001F"))
+ elsif value.to_s.include?("::")
+ model, id = value.split("::")
+ record = model.constantize.find_by(:id => id)
+ record ? [{:label => record.description, :value => record.id}] : value
+ else
+ value
+ end
+ end
+ end
+
+ def service_request_data(request_options, wf)
+ {
+ :dialogId => request_options[:workflow_settings][:dialog_id],
+ :requestDialogOptions => dialog_field_values(request_options[:dialog], wf),
+ }
end
end
diff --git a/app/helpers/order_service_helper.rb b/app/helpers/order_service_helper.rb
new file mode 100644
index 00000000000..121e0b80a2f
--- /dev/null
+++ b/app/helpers/order_service_helper.rb
@@ -0,0 +1,20 @@
+module OrderServiceHelper
+ def order_service_data(dialog)
+ {
+ :dialogId => dialog[:dialog_id],
+ :params => {
+ :resourceActionId => dialog[:resource_action_id],
+ :targetId => dialog[:target_id],
+ :targetType => dialog[:target_type],
+ :realTargetType => dialog[:real_target_type],
+ },
+ :urls => {
+ :apiSubmitEndpoint => dialog[:api_submit_endpoint],
+ :apiAction => dialog[:api_action],
+ :cancelEndPoint => dialog[:cancel_endpoint],
+ :finishSubmitEndpoint => dialog[:finish_submit_endpoint],
+ :openUrl => dialog[:open_url],
+ }
+ }
+ end
+end
diff --git a/app/javascript/components/service/DialogFields.jsx b/app/javascript/components/service/DialogFields.jsx
new file mode 100644
index 00000000000..a301fae4130
--- /dev/null
+++ b/app/javascript/components/service/DialogFields.jsx
@@ -0,0 +1,77 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { DIALOG_FIELD_TYPES } from './constants';
+import CheckboxField from './dialogFieldItems/CheckboxField';
+import DateField from './dialogFieldItems/DateField';
+import DateTimeField from './dialogFieldItems/DateTimeField';
+import DropDownField from './dialogFieldItems/DropDownField';
+import RadioField from './dialogFieldItems/RadioField';
+import RefreshField from './RefreshField';
+import ServiceContext from './ServiceContext';
+import TagField from './dialogFieldItems/TagField';
+import TextInputField from './dialogFieldItems/TextInputField';
+import TextAreaField from './dialogFieldItems/TextAreaField';
+
+/** Function to render fields based on type */
+const renderFieldContent = (field) => {
+ switch (field.type) {
+ case DIALOG_FIELD_TYPES.checkBox:
+ return ;
+ case DIALOG_FIELD_TYPES.date:
+ return ;
+ case DIALOG_FIELD_TYPES.dateTime:
+ return ;
+ case DIALOG_FIELD_TYPES.dropDown:
+ return ;
+ case DIALOG_FIELD_TYPES.radio:
+ return ;
+ case DIALOG_FIELD_TYPES.tag:
+ return ;
+ case DIALOG_FIELD_TYPES.textBox:
+ return ;
+ case DIALOG_FIELD_TYPES.textArea:
+ return ;
+ default:
+ return <>{__('Field not supported')}>;
+ }
+};
+
+/** Function to render a field. */
+const renderFieldItem = (field, data) => {
+ const isRefreshing = data.fieldsToRefresh.includes(field.name);
+ return (
+
+
+ {
+ renderFieldContent(field)
+ }
+
+
+
+ );
+};
+
+/** Component to render the Fields in the Service/DialogTabs/DialogGroups component */
+const DialogFields = ({ dialogFields }) => {
+ const { data } = useContext(ServiceContext);
+ return (
+ <>
+ {
+ dialogFields.map((field) => (
+ field.visible ? renderFieldItem(field, data) :
+ ))
+ }
+ >
+ );
+};
+
+DialogFields.propTypes = {
+ dialogFields: PropTypes.arrayOf(PropTypes.any).isRequired,
+};
+
+export default DialogFields;
diff --git a/app/javascript/components/service/DialogGroups.jsx b/app/javascript/components/service/DialogGroups.jsx
new file mode 100644
index 00000000000..b50cd77091b
--- /dev/null
+++ b/app/javascript/components/service/DialogGroups.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import DialogFields from './DialogFields';
+
+/** Component to render the Groups in the Service/DialogTabs component */
+const DialogGroups = ({ dialogGroups }) => (
+ <>
+ {
+ dialogGroups.map((item) => (
+
+
+ {item.label}
+
+
+
+
+
+ ))
+ }
+ >
+);
+
+DialogGroups.propTypes = {
+ dialogGroups: PropTypes.arrayOf(PropTypes.any).isRequired,
+};
+
+export default DialogGroups;
diff --git a/app/javascript/components/service/DialogTabs.jsx b/app/javascript/components/service/DialogTabs.jsx
new file mode 100644
index 00000000000..b2c8123d7d7
--- /dev/null
+++ b/app/javascript/components/service/DialogTabs.jsx
@@ -0,0 +1,40 @@
+import React, { useContext } from 'react';
+import { Tabs, Tab, Loading } from 'carbon-components-react';
+import DialogGroups from './DialogGroups';
+import ServiceContext from './ServiceContext';
+import { extractDialogTabs } from './helper';
+
+/** Component to render the Tabs in the Service component */
+const DialogTabs = () => {
+ const { data } = useContext(ServiceContext);
+ const dialogTabs = extractDialogTabs(data.apiResponse);
+
+ const tabLabel = (label, tabIndex) => {
+ const { fieldsToRefresh, groupFieldsByTab } = data;
+ const refreshInProgress = fieldsToRefresh.some((field) => groupFieldsByTab[tabIndex].includes(field));
+ return refreshInProgress
+ ? (
+
+ {label}
+
+
+ )
+ : label;
+ };
+
+ return (
+
+ {
+ dialogTabs.map((tab, tabIndex) => (
+
+
+
+
+
+ ))
+ }
+
+ );
+};
+
+export default DialogTabs;
diff --git a/app/javascript/components/service/RefreshField.jsx b/app/javascript/components/service/RefreshField.jsx
new file mode 100644
index 00000000000..523c4422c35
--- /dev/null
+++ b/app/javascript/components/service/RefreshField.jsx
@@ -0,0 +1,62 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Loading } from 'carbon-components-react';
+import { Renew16 } from '@carbon/icons-react';
+import ServiceContext from './ServiceContext';
+import ServiceValidator from './ServiceValidator';
+import { defaultFieldValue } from './helper';
+import { fieldProperties } from './helper.field';
+
+/** Function to reset the dialogField data when the field refresh button is clicked. */
+const resetDialogField = (dialogFields, field) => {
+ const { value, valid } = ServiceValidator.validateField({ field, value: defaultFieldValue(field) });
+ dialogFields[field.name] = { value, valid };
+ return { ...dialogFields };
+};
+
+const RefreshField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const { isDisabled } = fieldProperties(field, data);
+
+ const { fieldsToRefresh } = data;
+ const inProgress = fieldsToRefresh.includes(field.name);
+ return (
+
+ {
+ !!(field.dynamic && field.show_refresh_button) && !inProgress && (
+
+ );
+};
+
+RefreshField.propTypes = {
+ field: PropTypes.shape({
+ label: PropTypes.string,
+ dynamic: PropTypes.bool,
+ show_refresh_button: PropTypes.bool,
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ name: PropTypes.string,
+ }).isRequired,
+};
+
+export default RefreshField;
diff --git a/app/javascript/components/service/ServiceButtons.jsx b/app/javascript/components/service/ServiceButtons.jsx
new file mode 100644
index 00000000000..a2a9ab6d28a
--- /dev/null
+++ b/app/javascript/components/service/ServiceButtons.jsx
@@ -0,0 +1,84 @@
+import React, { useContext, useEffect } from 'react';
+import { Button } from 'carbon-components-react';
+import ServiceContext from './ServiceContext';
+import { omitValidation } from './helper';
+import miqRedirectBack from '../../helpers/miq-redirect-back';
+
+const ServiceButtons = React.memo(() => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ apiAction, apiSubmitEndpoint, openUrl, finishSubmitEndpoint,
+ } = data.urls;
+
+ useEffect(() => {
+ if (data.locked) {
+ const handleSubmission = async() => {
+ const values = omitValidation(data.dialogFields);
+ let submitData = { action: 'order', ...values };
+
+ if (apiSubmitEndpoint.includes('/generic_objects/')) {
+ submitData = { action: apiAction, parameters: values };
+ } else if (apiAction === 'reconfigure') {
+ submitData = { action: apiAction, resource: values };
+ }
+
+ try {
+ const response = await API.post(apiSubmitEndpoint, submitData, { skipErrors: [400] });
+ if (openUrl === 'true') {
+ const taskResponse = await API.wait_for_task(response)
+ .then(() =>
+ // eslint-disable-next-line no-undef
+ $http.post('open_url_after_dialog', { targetId, realTargetType }));
+
+ 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();
+ }
+ } else {
+ miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint);
+ }
+ } catch (_error) {
+ // Handle error if needed
+ }
+ };
+
+ handleSubmission();
+ }
+ }, [data.locked]);
+
+ const formValid = data.dialogFields ? Object.values(data.dialogFields).every((field) => field.valid) : false;
+
+ const submitForm = () => {
+ miqSparkleOn();
+ setData({
+ ...data,
+ locked: true,
+ });
+ };
+
+ return (
+
+
+
+
+
+ );
+});
+
+export default ServiceButtons;
diff --git a/app/javascript/components/service/ServiceContext.jsx b/app/javascript/components/service/ServiceContext.jsx
new file mode 100644
index 00000000000..47b48de8721
--- /dev/null
+++ b/app/javascript/components/service/ServiceContext.jsx
@@ -0,0 +1,4 @@
+import { createContext } from 'react';
+
+const ServiceContext = createContext();
+export default ServiceContext;
diff --git a/app/javascript/components/service/ServiceValidator.js b/app/javascript/components/service/ServiceValidator.js
new file mode 100644
index 00000000000..86f2d194616
--- /dev/null
+++ b/app/javascript/components/service/ServiceValidator.js
@@ -0,0 +1,117 @@
+import { DIALOG_FIELD_TYPES, ServiceType } from './constants';
+
+class ServiceValidator {
+ constructor(serviceType) {
+ if (!ServiceValidator.instance) {
+ this.serviceType = serviceType;
+ ServiceValidator.instance = this;
+ }
+ return ServiceValidator.instance;
+ }
+
+ static validateField(data) {
+ switch (ServiceValidator.instance.serviceType) {
+ case ServiceType.order:
+ return this.orderServiceValidation(data);
+ case ServiceType.dialog:
+ case ServiceType.request:
+ default:
+ return ({ valid: true, value: data.value });
+ }
+ }
+
+ static orderServiceValidation(data) {
+ const validationMap = {
+ [DIALOG_FIELD_TYPES.checkBox]: this.checkbox,
+ [DIALOG_FIELD_TYPES.date]: this.date,
+ [DIALOG_FIELD_TYPES.dateTime]: this.dateTime,
+ [DIALOG_FIELD_TYPES.dropDown]: this.dropDown,
+ [DIALOG_FIELD_TYPES.radio]: this.radio,
+ [DIALOG_FIELD_TYPES.tag]: this.tag,
+ [DIALOG_FIELD_TYPES.textBox]: this.textBox,
+ [DIALOG_FIELD_TYPES.textArea]: this.textArea,
+ };
+ const validateFn = validationMap[data.field.type] || this.default;
+ return validateFn.call(this, data);
+ }
+
+ static checkbox(data) {
+ return { valid: (data.field.required ? !!data.value : true), value: data.value !== '' };
+ }
+
+ static date({ field, value }) {
+ const { day, month, year } = value;
+ const hasDate = !!((day && month && year));
+ return { valid: field.required ? hasDate : true, value: { day, month, year } };
+ }
+
+ static time({ field, value }) {
+ const { hour, minute, meridiem } = value;
+ const hasMinute = !!((hour && { hour, minute, meridiem }));
+ return { valid: field.required ? hasMinute : true, value: { hour, minute, meridiem } };
+ }
+
+ static dateTime(data) {
+ const date = this.date(data);
+ const time = this.time(data);
+ return { valid: data.field.required ? !!(date.valid && time.valid) : true, value: { ...date.value, ...time.value } };
+ }
+
+ static dropDown(data) {
+ const isMulti = !!(data.field.options && data.field.options.force_multi_value);
+ const valid = isMulti ? !!(data.value.length > 0) : !!data.value.id;
+ return { valid: data.field.required ? valid : true, value: data.value };
+ }
+
+ static radio(data) {
+ return { valid: data.field.required ? !!data.value : true, value: data.value };
+ }
+
+ static tag(data) {
+ const isMulti = !!(data.field.options && !data.field.options.force_single_value);
+ const valid = isMulti ? !!(data.value.length > 0) : !!data.value.id;
+ return { valid: data.field.required ? valid : true, value: data.value };
+ }
+
+ static textBox({ field, value }) {
+ return this.commonFieldValidation(field, value);
+ }
+
+ static textArea({ field, value }) {
+ return this.commonFieldValidation(field, value);
+ }
+
+ /** Regular expression is handled only for TextInput and TextArea */
+ static regularExpressionValidation(field, value) {
+ if (field.validator_type === 'regex' && field.validator_rule) {
+ try {
+ if (typeof field.validator_rule === 'string') {
+ field.validator_rule = new RegExp(field.validator_rule);
+ }
+ const aaa = field.validator_rule.test(value);
+ return aaa ? { valid: true, value, message: undefined }
+ : { valid: false, message: field.validator_message || __('Custom Validation failed'), value };
+ } catch (error) {
+ console.error('Unexpected error occurred when the field was validated using regular expression.', error);
+ throw error;
+ }
+ }
+ return undefined;
+ }
+
+ /** Validation for TextInput, TextArea. */
+ static commonFieldValidation(field, value) {
+ const custom = this.regularExpressionValidation(field, value);
+ if (field.required) {
+ if (custom) {
+ return custom.valid
+ ? { valid: true, value }
+ : { valid: false, value, message: custom.message };
+ }
+ return { valid: !!value, value };
+ }
+ return { valid: true, value };
+ }
+}
+
+export default ServiceValidator;
diff --git a/app/javascript/components/service/constants.js b/app/javascript/components/service/constants.js
new file mode 100644
index 00000000000..d5c2178db29
--- /dev/null
+++ b/app/javascript/components/service/constants.js
@@ -0,0 +1,21 @@
+export const DIALOG_FIELD_TYPES = {
+ checkBox: 'DialogFieldCheckBox',
+ date: 'DialogFieldDateControl',
+ dateTime: 'DialogFieldDateTimeControl',
+ dropDown: 'DialogFieldDropDownList',
+ radio: 'DialogFieldRadioButton',
+ tag: 'DialogFieldTagControl',
+ textBox: 'DialogFieldTextBox',
+ textArea: 'DialogFieldTextAreaBox',
+};
+
+export const ServiceType = {
+ order: 'orderServiceForm',
+ dialog: 'serviceDialog',
+ request: 'serviceRequest',
+};
+
+export const RefreshStatus = {
+ notStarted: 'notStarted',
+ completed: 'completed',
+};
diff --git a/app/javascript/components/service/dialogFieldItems/CheckboxField.jsx b/app/javascript/components/service/dialogFieldItems/CheckboxField.jsx
new file mode 100644
index 00000000000..8ada161e85f
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/CheckboxField.jsx
@@ -0,0 +1,58 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { Checkbox } from 'carbon-components-react';
+import { fieldProperties } from '../helper.field';
+import FieldLabel from './FieldLabel';
+import ServiceContext from '../ServiceContext';
+import ServiceValidator from '../ServiceValidator';
+
+/** Component to render the Checkbox in the Service/DialogTabs/DialogGroups/DialogFields component */
+const CheckboxField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+
+ /** Checkbox onChange event handler */
+ const onChange = (checked) => {
+ if (data.isOrderServiceForm) {
+ const { valid, value } = ServiceValidator.validateField({ value: checked, field });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ }
+ };
+
+ return (
+
+
}
+ onChange={(checked) => onChange(checked)}
+ readOnly={field.read_only}
+ />
+ {
+ !fieldData.valid &&
{requiredLabel}
+ }
+
+ );
+};
+
+CheckboxField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ default_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ label: PropTypes.string,
+ name: PropTypes.string,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+};
+
+export default CheckboxField;
diff --git a/app/javascript/components/service/dialogFieldItems/DateField.jsx b/app/javascript/components/service/dialogFieldItems/DateField.jsx
new file mode 100644
index 00000000000..b9bcd276561
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/DateField.jsx
@@ -0,0 +1,68 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { DatePicker, DatePickerInput } from 'carbon-components-react';
+import { fieldProperties } from '../helper.field';
+import { extractDate, dateString } from '../helper.dateTime';
+import ServiceContext from '../ServiceContext';
+import ServiceValidator from '../ServiceValidator';
+
+/** Component to render the Radio buttons in the Service/DialogTabs/DialogGroups/DialogFields component */
+const DateField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+ const selectedDate = dateString(fieldData.value);
+
+ /** DatePicker's onChange event handler */
+ const onChange = (selectedItem) => {
+ if (data.isOrderServiceForm) {
+ const extractedDate = extractDate(selectedItem[0]);
+ const { valid, value } = ServiceValidator.validateField({ field, value: extractedDate });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+};
+
+DateField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ default_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ values: PropTypes.arrayOf(PropTypes.any),
+ label: PropTypes.string,
+ name: PropTypes.string,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+};
+
+export default DateField;
diff --git a/app/javascript/components/service/dialogFieldItems/DateTimeField.jsx b/app/javascript/components/service/dialogFieldItems/DateTimeField.jsx
new file mode 100644
index 00000000000..50b5c971ede
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/DateTimeField.jsx
@@ -0,0 +1,133 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { DatePicker, Dropdown, DatePickerInput } from 'carbon-components-react';
+import { fieldProperties } from '../helper.field';
+import { currentDateTime, dateTimeString, extractDateTime } from '../helper.dateTime';
+import ServiceContext from '../ServiceContext';
+import ServiceValidator from '../ServiceValidator';
+
+/** Component to render the Radio buttons in the Service/DialogTabs/DialogGroups/DialogFields component */
+const DateTimeField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+ const selectedDateTime = dateTimeString(fieldData.value);
+ const { hours, minutes } = currentDateTime();
+
+ const updateDateTime = (fieldData) => {
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], ...fieldData };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ };
+
+ /** Function to handle the hour's onChange event. */
+ const onHourChange = ({ selectedItem }) => {
+ if (data.isOrderServiceForm) {
+ fieldData.value.hour = selectedItem.id;
+ updateDateTime({ ...fieldData });
+ }
+ };
+
+ /** Function to handle the minute's onChange event. */
+ const onMinuteChange = ({ selectedItem }) => {
+ if (data.isOrderServiceForm) {
+ fieldData.value.minute = selectedItem.id;
+ updateDateTime({ ...fieldData });
+ }
+ };
+
+ /** Function to handle the AM/PM onChange event. */
+ const onMeridiemChange = ({ selectedItem }) => {
+ if (data.isOrderServiceForm) {
+ fieldData.value.meridiem = selectedItem.id;
+ updateDateTime({ ...fieldData });
+ }
+ };
+
+ /** Function to handle the date's onChange event. */
+ const onDateChange = (selectedItem) => {
+ if (data.isOrderServiceForm) {
+ const extractedDateTime = extractDateTime(selectedItem[0], selectedDateTime);
+ const { valid, value } = ServiceValidator.validateField({ field, value: extractedDateTime });
+ updateDateTime({ value, valid, type: field.type });
+ }
+ };
+
+ return (
+
+
+
+
+ (item ? item.text : '')}
+ onChange={onHourChange}
+ readOnly={field.read_only}
+ />
+ (item ? item.text : '')}
+ onChange={onMinuteChange}
+ readOnly={field.read_only}
+ />
+ ({ id: item, text: item }))}
+ itemToString={(item) => (item ? item.text : '')}
+ onChange={onMeridiemChange}
+ readOnly={field.read_only}
+ />
+
+ );
+};
+
+DateTimeField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ default_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ values: PropTypes.arrayOf(PropTypes.any),
+ label: PropTypes.string,
+ name: PropTypes.string,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+};
+
+export default DateTimeField;
diff --git a/app/javascript/components/service/dialogFieldItems/DropDownField.jsx b/app/javascript/components/service/dialogFieldItems/DropDownField.jsx
new file mode 100644
index 00000000000..7220ebf9555
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/DropDownField.jsx
@@ -0,0 +1,34 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import MultiDropDownField from './DropDowns/MultiDropDownField';
+import SimpleDropDownField from './DropDowns/SimpleDropDownField';
+import ServiceContext from '../ServiceContext';
+import { defaultFieldOptions } from '../helper';
+
+/** Component to render the DropDownField in the Service/DialogTabs/DialogGroups/DialogFields component */
+const DropDownField = ({ field }) => {
+ const { data } = useContext(ServiceContext);
+ const isMulti = !!(field.options && field.options.force_multi_value);
+ const options = defaultFieldOptions(field, data);
+
+ return isMulti
+ ?
+ : ;
+};
+
+DropDownField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ options: PropTypes.shape({
+ force_multi_value: PropTypes.bool,
+ }),
+ default_value: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ type: PropTypes.string,
+ values: PropTypes.arrayOf(PropTypes.any),
+ required: PropTypes.bool,
+ }).isRequired,
+};
+
+export default DropDownField;
diff --git a/app/javascript/components/service/dialogFieldItems/DropDowns/MultiDropDownField.jsx b/app/javascript/components/service/dialogFieldItems/DropDowns/MultiDropDownField.jsx
new file mode 100644
index 00000000000..b2f6e8f0412
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/DropDowns/MultiDropDownField.jsx
@@ -0,0 +1,70 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { FilterableMultiSelect } from 'carbon-components-react';
+import { fieldProperties } from '../../helper.field';
+import FieldLabel from '../FieldLabel';
+import ServiceValidator from '../../ServiceValidator';
+import ServiceContext from '../../ServiceContext';
+
+/** Component to render the MultiDropDownField in the Service/DialogTabs/DialogGroups/DialogFields component */
+const MultiDropDownField = ({ field, options }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+
+ /** FilterableMultiSelect onChange event handler */
+ const onChange = ({ selectedItems }) => {
+ if (data.isOrderServiceForm) {
+ const { valid, value } = ServiceValidator.validateField({ value: selectedItems, field });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ }
+ };
+
+ /** Custom sort function that does nothing to maintain the order of items. sorting is already handled in helper functions.
+ * Or we han avoid the helper functions mad implement a logic here using the field.options.sort_order property. (easy way).
+ */
+ const noSort = (items) => items;
+
+ return (
+ }
+ initialSelectedItems={fieldData.value}
+ placeholder={fieldData.value.length <= 0 ? __('Nothing selected') : ''}
+ invalidText={requiredLabel}
+ items={options.map((item) => ({ ...item, label: item.text }))}
+ itemToString={(item) => (item ? item.text : '')}
+ onChange={onChange}
+ selectionFeedback="top-after-reopen"
+ readOnly={field.read_only}
+ sortItems={noSort}
+ />
+ );
+};
+
+MultiDropDownField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ name: PropTypes.string,
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ label: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ text: PropTypes.string.isRequired,
+ label: PropTypes.string,
+ })).isRequired,
+};
+
+export default MultiDropDownField;
diff --git a/app/javascript/components/service/dialogFieldItems/DropDowns/SimpleDropDownField.jsx b/app/javascript/components/service/dialogFieldItems/DropDowns/SimpleDropDownField.jsx
new file mode 100644
index 00000000000..ebb8a057df0
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/DropDowns/SimpleDropDownField.jsx
@@ -0,0 +1,68 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { Dropdown } from 'carbon-components-react';
+import { fieldProperties } from '../../helper.field';
+import FieldLabel from '../FieldLabel';
+import ServiceContext from '../../ServiceContext';
+import ServiceValidator from '../../ServiceValidator';
+
+/** Component to render the SimpleDropDownField in the Service/DialogTabs/DialogGroups/DialogFields component */
+const SimpleDropDownField = ({ field, options }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+
+ /** Dropdown onChange event handler */
+ const onChange = ({ selectedItem }) => {
+ const { valid, value } = ServiceValidator.validateField({ value: selectedItem, field });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid };
+ if (data.isOrderServiceForm) {
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ } else {
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ });
+ }
+ };
+
+ return (
+ }
+ initialSelectedItem={options[0]}
+ selectedItem={fieldData.value}
+ invalidText={requiredLabel}
+ label={__('Nothing selected')}
+ items={options}
+ itemToString={(item) => (item ? item.text : '')}
+ onChange={onChange}
+ readOnly={field.read_only}
+ />
+ );
+};
+
+SimpleDropDownField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ name: PropTypes.string,
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ label: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+ options: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ text: PropTypes.string.isRequired,
+ })).isRequired,
+};
+
+export default SimpleDropDownField;
diff --git a/app/javascript/components/service/dialogFieldItems/FieldLabel.jsx b/app/javascript/components/service/dialogFieldItems/FieldLabel.jsx
new file mode 100644
index 00000000000..51101e422d8
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/FieldLabel.jsx
@@ -0,0 +1,55 @@
+import React, { useMemo, useContext } from 'react';
+import PropTypes from 'prop-types';
+import { InformationFilled16 } from '@carbon/icons-react';
+import { TooltipIcon, Tag } from 'carbon-components-react';
+import ServiceContext from '../ServiceContext';
+
+/** Function to render a Tag when the field refresh is in progress. */
+const refreshingLabel = (label) => {__(`Refreshing ${label}...`)};
+
+/** Function to render a ToolTip when a field description is available. */
+const getTooltip = (description) => (
+ description ? (
+
+
+
+
+
+ ) : null
+);
+
+/** Function to render refreshing label for a field. */
+const getFieldLabelContent = (field, data) => {
+ if (data.fieldsToRefresh.includes(field.name)) {
+ return refreshingLabel(field.label);
+ }
+ return (
+ <>
+ {data.isOrderServiceForm && field.required && *}
+ {field.label}
+ {getTooltip(field.description)}
+ >
+ );
+};
+
+/** Component to render the fields label */
+const FieldLabel = React.memo(({ field }) => {
+ const { data } = useContext(ServiceContext);
+ const labelContent = useMemo(() => getFieldLabelContent(field, data), [field, data]);
+ return (
+
+ {labelContent}
+
+ );
+});
+
+FieldLabel.propTypes = {
+ field: PropTypes.shape({
+ label: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ name: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ }).isRequired,
+};
+
+export default FieldLabel;
diff --git a/app/javascript/components/service/dialogFieldItems/RadioField.jsx b/app/javascript/components/service/dialogFieldItems/RadioField.jsx
new file mode 100644
index 00000000000..a962a3704e3
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/RadioField.jsx
@@ -0,0 +1,72 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { RadioButtonGroup, RadioButton } from 'carbon-components-react';
+import { fieldProperties } from '../helper.field';
+import FieldLabel from './FieldLabel';
+import ServiceContext from '../ServiceContext';
+import ServiceValidator from '../ServiceValidator';
+
+/** Component to render the Radio buttons in the Service/DialogTabs/DialogGroups/DialogFields component */
+const RadioField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+
+ /** Function to handle the RadioButton's onChange event. */
+ const onChange = (selectedValue) => {
+ if (data.isOrderServiceForm) {
+ const { valid, value } = ServiceValidator.validateField({ value: selectedValue, field });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ }
+ };
+
+ return (
+
+
}
+ name={field.name}
+ id={fieldId}
+ readOnly={field.read_only}
+ invalid={!fieldData.valid}
+ invalidText={requiredLabel}
+ onChange={onChange}
+ valueSelected={fieldData.value} // Ensure the selected value is correctly handled
+ >
+ {
+ field.values.map((radio) => (
+
+ ))
+ }
+
+ {field.required && !fieldData.valid &&
{requiredLabel}
}
+
+ );
+};
+
+RadioField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ type: PropTypes.string,
+ default_value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
+ values: PropTypes.arrayOf(PropTypes.any),
+ label: PropTypes.string,
+ name: PropTypes.string,
+ required: PropTypes.bool,
+ read_only: PropTypes.bool,
+ }).isRequired,
+};
+
+export default RadioField;
diff --git a/app/javascript/components/service/dialogFieldItems/TagField.jsx b/app/javascript/components/service/dialogFieldItems/TagField.jsx
new file mode 100644
index 00000000000..d744073f897
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/TagField.jsx
@@ -0,0 +1,34 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import MultiDropDownField from './DropDowns/MultiDropDownField';
+import SimpleDropDownField from './DropDowns/SimpleDropDownField';
+import ServiceContext from '../ServiceContext';
+import { defaultFieldOptions } from '../helper';
+
+/** Component to render the TagField in the Service/DialogTabs/DialogGroups/DialogFields component */
+const TagField = ({ field }) => {
+ const { data } = useContext(ServiceContext);
+ const isMulti = !!(field.options && !field.options.force_single_value);
+ const options = defaultFieldOptions(field, data);
+ return isMulti
+ ?
+ : ;
+};
+
+TagField.propTypes = {
+ field: PropTypes.shape({
+ id: PropTypes.string,
+ options: PropTypes.shape({
+ force_multi_value: PropTypes.bool,
+ force_single_value: PropTypes.bool,
+ }),
+ default_value: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ type: PropTypes.string,
+ values: PropTypes.arrayOf(PropTypes.any),
+ required: PropTypes.bool,
+ }).isRequired,
+};
+
+export default TagField;
diff --git a/app/javascript/components/service/dialogFieldItems/TextAreaField.jsx b/app/javascript/components/service/dialogFieldItems/TextAreaField.jsx
new file mode 100644
index 00000000000..a3aea572b19
--- /dev/null
+++ b/app/javascript/components/service/dialogFieldItems/TextAreaField.jsx
@@ -0,0 +1,57 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import { TextArea } from 'carbon-components-react';
+import { fieldProperties } from '../helper.field';
+import FieldLabel from './FieldLabel';
+import ServiceContext from '../ServiceContext';
+import ServiceValidator from '../ServiceValidator';
+
+/** Component to render the TextAreaField in the Service/DialogTabs/DialogGroups/DialogFields component */
+const TextAreaField = ({ field }) => {
+ const { data, setData } = useContext(ServiceContext);
+ const {
+ fieldData, isDisabled, fieldId, requiredLabel,
+ } = fieldProperties(field, data);
+
+ /** Function to handle the TextArea's onChange event. */
+ const onChange = (event) => {
+ if (data.isOrderServiceForm) {
+ const { valid, value, message } = ServiceValidator.validateField({ value: event.target.value, field });
+ data.dialogFields[field.name] = { ...data.dialogFields[field.name], value, valid, message };
+ setData({
+ ...data,
+ dialogFields: { ...data.dialogFields },
+ fieldsToRefresh: field.dialog_field_responders,
+ });
+ }
+ };
+
+ return (
+