diff --git a/app/javascript/components/automate-entry-points/index.jsx b/app/javascript/components/automate-entry-points/index.jsx index e5e5f805152..ad2892319f0 100644 --- a/app/javascript/components/automate-entry-points/index.jsx +++ b/app/javascript/components/automate-entry-points/index.jsx @@ -40,7 +40,7 @@ const AutomateEntryPoints = ({ }; useEffect(() => { - if (selectedValue.element) { + if (selectedValue && selectedValue.element) { data.forEach((node) => { if (node.id === selectedValue.element.id) { document.getElementById(node.id).classList.add('currently-selected'); diff --git a/app/javascript/components/automate-entry-points/styles.css b/app/javascript/components/automate-entry-points/styles.css index c011206c068..b1b781d6c80 100644 --- a/app/javascript/components/automate-entry-points/styles.css +++ b/app/javascript/components/automate-entry-points/styles.css @@ -74,6 +74,10 @@ margin-bottom: 1rem; } +.bx--modal-container { + overflow-x: scroll; +} + .bx--btn--primary { margin-right: 10px; } diff --git a/app/javascript/components/common/inline-flash-message/index.jsx b/app/javascript/components/common/inline-flash-message/index.jsx new file mode 100644 index 00000000000..249579880f2 --- /dev/null +++ b/app/javascript/components/common/inline-flash-message/index.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { InlineNotification } from 'carbon-components-react'; + +/** + * Inline flash message for showing notifications. + * + * @param {Object} message - The notification details to display (kind, title, subtitle). + * If `null` or `undefined`, no notification is shown. + * @param {Function} onCloseClick - Callback for handling close button clicks. + * @param {boolean} showCloseButton - Whether to display the close button. + */ +const InlineFlashMessage = ({ message, onCloseClick, showCloseButton }) => { + if (!message) return null; + + return ( + + ); +}; + +InlineFlashMessage.propTypes = { + message: PropTypes.shape({ + kind: PropTypes.oneOf(['success', 'error', 'info', 'warning']).isRequired, + title: PropTypes.string, + subtitle: PropTypes.string, + }).isRequired, + onCloseClick: PropTypes.func, + showCloseButton: PropTypes.bool, +}; + +InlineFlashMessage.defaultProps = { + onCloseClick: () => {}, + showCloseButton: true, +}; + +export default InlineFlashMessage; diff --git a/app/javascript/components/date-time-picker/index.jsx b/app/javascript/components/date-time-picker/index.jsx new file mode 100644 index 00000000000..1fa657631c6 --- /dev/null +++ b/app/javascript/components/date-time-picker/index.jsx @@ -0,0 +1,107 @@ +import React, { useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + DatePicker, + DatePickerInput, + TimePicker, + TimePickerSelect, + SelectItem, + FormLabel, +} from 'carbon-components-react'; +import { getCurrentDate, getCurrentTimeAndPeriod } from '../service-dialog-form/helper'; + +const CustomDateTimePicker = ({ label, onChange, initialData }) => { + const [date, setDate] = useState(() => initialData.date || getCurrentDate()); + const [time, setTime] = useState(() => initialData.time || getCurrentTimeAndPeriod().time); + const [isValid, setIsValid] = useState(true); + const [period, setPeriod] = useState(() => initialData.period || getCurrentTimeAndPeriod().period); + + const handleDateChange = (newDate) => { + if (newDate && newDate.length) { + const formattedDate = new Intl.DateTimeFormat('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + }).format(newDate[0]); + setDate(formattedDate); + onChange({ value: `${formattedDate} ${time} ${period}`, initialData }); + } + }; + + // Function to validate the time input + const validateTime = (value) => { + const timeRegex = /^(0[1-9]|1[0-2]):[0-5][0-9]$/; // Matches 12-hour format hh:mm + return timeRegex.test(value); + }; + + const handleTimeChange = (event) => { + const newTime = event.target.value; + setTime(newTime); + const isValidTime = validateTime(newTime); + setIsValid(isValidTime) + if (isValidTime) onChange({ value: `${date} ${newTime} ${period}`, initialData }); + }; + + const handlePeriodChange = (event) => { + const newPeriod = event.target.value; + setPeriod(newPeriod); + onChange({ value: `${date} ${time} ${newPeriod}`, initialData }); + }; + + return ( +
+ {label} + + + + + + + + + +
+ ); +}; + +CustomDateTimePicker.propTypes = { + initialData: PropTypes.shape({ + date: PropTypes.string, + time: PropTypes.string, + period: PropTypes.string, + }), + onChange: PropTypes.func, + label: PropTypes.string, +}; + +CustomDateTimePicker.defaultProps = { + initialData: { date: '', time: '', period: '' }, + onChange: () => {}, + label: '', +}; + +export default CustomDateTimePicker; diff --git a/app/javascript/components/embedded-automate-entry-point/index.jsx b/app/javascript/components/embedded-automate-entry-point/index.jsx index d7e70c88c28..3561c1ee94a 100644 --- a/app/javascript/components/embedded-automate-entry-point/index.jsx +++ b/app/javascript/components/embedded-automate-entry-point/index.jsx @@ -36,11 +36,30 @@ const EmbeddedAutomateEntryPoint = (props) => { }, [selectedValue, includeDomainPrefix]); useEffect(() => { - if (selectedValue && selectedValue.name && selectedValue.name.text) { - selectedValue.name.text = textValue; + if (selectedValue && selectedValue.name && selectedValue.name.text) { + if (selectedValue.name.text !== textValue) { + const updatedValue = { + ...selectedValue, + name: { + ...selectedValue.name, + text: textValue + } + }; + + // Update the form without triggering another effect run + input.onChange(updatedValue); + + // Update the state + setSelectedValue(updatedValue); } + } else if (!selectedValue || Object.keys(selectedValue || {}).length === 0) { + // When selectedValue is empty or undefined, pass null to trigger validation + input.onChange(null); + } else { input.onChange(selectedValue); - }, [textValue]); + } +}, [textValue]); + return (
@@ -56,7 +75,12 @@ const EmbeddedAutomateEntryPoint = (props) => { />
- setTextValue(value.target.value)} value={textValue} /> +
@@ -73,8 +97,14 @@ const EmbeddedAutomateEntryPoint = (props) => { iconDescription={sprintf(__('Remove this %s'), label)} hasIconOnly onClick={() => { - setSelectedValue({}); + setSelectedValue(null); setTextValue(''); + // Ensure the input change is triggered to update form state + // input.onChange(null); + input.onChange({ + automateEntryPoint: null, + value: null // Also clear any associated value + }); }} />
diff --git a/app/javascript/components/embedded-workflow-entry-point/index.jsx b/app/javascript/components/embedded-workflow-entry-point/index.jsx index db3d08a4f04..c98f4fd171c 100644 --- a/app/javascript/components/embedded-workflow-entry-point/index.jsx +++ b/app/javascript/components/embedded-workflow-entry-point/index.jsx @@ -26,9 +26,14 @@ const EmbeddedWorkflowEntryPoint = (props) => { useEffect(() => { if (selectedValue && selectedValue.name && selectedValue.name.text) { selectedValue.name.text = textValue; + input.onChange(selectedValue); + } else if (!selectedValue || Object.keys(selectedValue).length === 0) { + // When selectedValue is empty or undefined, pass null to trigger validation + input.onChange(null); + } else { + input.onChange(selectedValue); } - input.onChange(selectedValue); - }, [textValue]); + }, [textValue, selectedValue]); return (
@@ -43,7 +48,7 @@ const EmbeddedWorkflowEntryPoint = (props) => { ) : undefined}
- setTextValue(value.target.value)} value={textValue} /> + setTextValue(value.target.value)} value={textValue} readOnly />
@@ -61,6 +66,8 @@ const EmbeddedWorkflowEntryPoint = (props) => { hasIconOnly onClick={() => { setSelectedValue({}); + // Ensure the input change is triggered to update form state + input.onChange(null); }} />
diff --git a/app/javascript/components/service-dialog-form/data.js b/app/javascript/components/service-dialog-form/data.js new file mode 100644 index 00000000000..d198ef15d24 --- /dev/null +++ b/app/javascript/components/service-dialog-form/data.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { + CheckboxChecked32, RadioButtonChecked32, Time32, StringText32, TextSmallCaps32, CaretDown32, Tag32, Calendar32, +} from '@carbon/icons-react'; +import { formattedCatalogPayload } from './helper'; + +export const dragItems = { + COMPONENT: 'component', + SECTION: 'section', + FIELD: 'field', + TAB: 'tab', +}; + +/** Data needed to render the dynamic components on the left hand side of the form. */ +export const dynamicComponents = [ + { id: 1, title: 'Text Box', icon: }, + { id: 2, title: 'Text Area', icon: }, + { id: 3, title: 'Check Box', icon: }, + { id: 4, title: 'Dropdown', icon: }, + { id: 5, title: 'Radio Button', icon: }, + { id: 6, title: 'Datepicker', icon: }, + { id: 7, title: 'Timepicker', icon: }, + { id: 8, title: 'Tag Control', icon: }, +]; + +/** Function which returns the default data for a section under a tab. */ +export const defaultSectionContents = (tabId, sectionId) => ({ + tabId, + sectionId, + title: 'New Section', + fields: [], + order: 0, +}); + +/** Function which returns the default data for a tab with default section. */ +export const defaultTabContents = (tabId) => ({ + tabId, + name: tabId === 0 ? __('New Tab') : __(`New Tab ${tabId}`), + sections: [defaultSectionContents(tabId, 0)], +}); + +/** Function to create a dummy tab for creating new tabs. */ +export const createNewTab = () => ({ + tabId: 'new', + name: 'Create Tab', + sections: [], +}); + +export const tagControlCategories = async() => { + try { + const { resources } = await API.get('/api/categories?expand=resources&attributes=id,name,description,single_value,children'); + + return resources; + } catch (error) { + console.error('Error fetching categories:', error); + return []; + } +}; + +// data has formfields and list (as of now); no dialog related general info - this is needed +export const saveServiceDialog = (data) => { + const payload = formattedCatalogPayload(data); + + API.post('/api/service_dialogs', payload, { + skipErrors: [400], + }).then(() => { + window.location.href = '/miq_ae_customization/explorer'; + }).catch((error) => { + console.error('Error saving dialog:', error); + }); +}; diff --git a/app/javascript/components/service-dialog-form/dynamic-component-chooser.jsx b/app/javascript/components/service-dialog-form/dynamic-component-chooser.jsx new file mode 100644 index 00000000000..39deb0a7d55 --- /dev/null +++ b/app/javascript/components/service-dialog-form/dynamic-component-chooser.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { dragItems } from './data'; + +/** Component to render the components list vertically on left side. + * Components can be used to drag and drop into the tab Contents */ +const DynamicComponentChooser = ({ list, onDragStartComponent }) => ( +
+ { + list.map((item, index) => ( +
onDragStartComponent(event, dragItems.COMPONENT)} + key={index.toString()} + > +
+ {item.icon} + {item.title} +
+
+ )) + } +
+); + +DynamicComponentChooser.propTypes = { + list: PropTypes.arrayOf(PropTypes.any).isRequired, + onDragStartComponent: PropTypes.func.isRequired, +}; + +export default DynamicComponentChooser; diff --git a/app/javascript/components/service-dialog-form/dynamic-field-actions.jsx b/app/javascript/components/service-dialog-form/dynamic-field-actions.jsx new file mode 100644 index 00000000000..9d383b99af0 --- /dev/null +++ b/app/javascript/components/service-dialog-form/dynamic-field-actions.jsx @@ -0,0 +1,117 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button } from 'carbon-components-react'; +import { Close16, Edit16 } from '@carbon/icons-react'; +import { SD_ACTIONS, SD_PROP_SHAPES } from './helper'; +import EditFieldModal from './edit-field-modal'; + +/** Component to render a Field. */ +const DynamicFieldActions = ({ + componentId, fieldProps, updateFieldProps, dynamicFieldAction, fieldConfiguration, dynamicToggleAction, setCategoryData, onValueChange, +}) => { + const [state, setState] = useState({ showModal: false }); + const { showModal } = state; + + const toggleModal = (show = false) => setState((state) => ({ ...state, showModal: show })); + // const onModalApply = () => setState((state) => ({ ...state, showModal: false })); + const onModalApply = (formValues, event) => { + setState((prevState) => ({ ...prevState, ...formValues })); + toggleModal(false); + dynamicFieldAction(event, formValues); + }; + + const onDynamicSwitchToggle = (isDynamic) => { + setState((prevState) => ({ ...prevState, dynamic: isDynamic })); + dynamicToggleAction(isDynamic); + }; + + const onTimePickerChange = (dateTime) => { + setState((prevState) => ({ ...prevState, value: dateTime })); + onValueChange(dateTime); + }; + + const onAutomationTypeChange = (val) => { + setState((prevState) => ({ ...prevState, automationType: val })); + onValueChange(val); + }; + + const renderEditButton = () => ( +