Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
90c4a97
Converts service dialogs from angular to react
elsamaryv Aug 25, 2025
078435b
Removes comments
elsamaryv Aug 25, 2025
89161ef
Removes commented(unwanted) code
elsamaryv Aug 25, 2025
5cbf8d7
Fix issue with embedded entrypoint modal
elsamaryv Aug 26, 2025
0e70609
Fixes issue with conditional rendering of entrypoint fields
elsamaryv Aug 26, 2025
e579fc7
Updates for entry_point_workflow field
elsamaryv Aug 26, 2025
527f929
Fixes sorting and adds description for component props
elsamaryv Aug 28, 2025
d5dd3b4
Updates InlineFlashMessage component
elsamaryv Aug 28, 2025
703cb11
Service dialog edit form
elsamaryv Sep 9, 2025
def500a
updated
elsamaryv Sep 10, 2025
2e80de6
fixes issues with displaying values and saving of service dialog edit…
elsamaryv Sep 10, 2025
16454f0
Updates cy test
elsamaryv Sep 11, 2025
dfca0a7
Cypress tests fixed for service dialog edit form
elsamaryv Sep 11, 2025
4f8d1b7
Removes log statements and commented lines
elsamaryv Sep 11, 2025
ed1acfc
updated prop name
elsamaryv Oct 7, 2025
f875f84
Sets message as required
elsamaryv Oct 7, 2025
88e730e
Updates on date-time picker components
elsamaryv Oct 9, 2025
3e276eb
updated
elsamaryv Oct 13, 2025
ce25cd2
Minor updates
elsamaryv Oct 22, 2025
93d7b93
Remove additional param
elsamaryv Oct 22, 2025
cc97620
Replaces separate onModalHide and onModalShow fns with a single fn
elsamaryv Oct 22, 2025
842d3c3
updates fieldSelector method
elsamaryv Oct 22, 2025
0ecf046
Refactors DynamicField component
elsamaryv Oct 22, 2025
d126ed3
removes comment
elsamaryv Oct 22, 2025
2842dee
Removes debugger
elsamaryv Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/components/automate-entry-points/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@
margin-bottom: 1rem;
}

.bx--modal-container {
overflow-x: scroll;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overflow-x: scroll might not be great on modals, it can mess with alignment when scrollbars show up unnecessarily.
I think overflow-x: auto would work better here.
Also, do we have a size prop for modals? Something like size="xl" (like bootstrap-modal does) to make it wider?

}

.bx--btn--primary {
margin-right: 10px;
}
Expand Down
43 changes: 43 additions & 0 deletions app/javascript/components/common/inline-flash-message/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<InlineNotification
kind={message.kind || 'info'} // "success" | "error" | "info" | "warning"
title={message.title || ''}
subtitle={message.subtitle || ''}
lowContrast
hideCloseButton={!showCloseButton}
onCloseButtonClick={onCloseClick}
/>
);
};

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;
107 changes: 107 additions & 0 deletions app/javascript/components/date-time-picker/index.jsx
Original file line number Diff line number Diff line change
@@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dealing with dates can be bug-prone. we should be fine for now with simple date logics but as logic grows, pulling in a utility like date-fns would help.


const CustomDateTimePicker = ({ label, onChange, initialData }) => {
const [date, setDate] = useState(() => initialData.date || getCurrentDate());
const [time, setTime] = useState(() => initialData.time || getCurrentTimeAndPeriod().time);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On initial render, time would look like this which is not in our expected "hh:mm" format, it should have been "09:54", But the invalid time error: "Enter a valid 12-hour time" is not displayed, probably thats because isValid state is initially true
image
Right after time field is edited the error is displayed since our state isValid state is now updated:
image

No issues with this implementation, this should work well once we apply zero-padding to the hour value in getCurrentTimeAndPeriod(similar to what we did with minutes).
It should be padded after converting to 12-hour format, otherwise, the modulo will reduce it to a single digit again.
hours = ${hours % 12 || 12}.padStart(2, '0');
Which should work well:
image

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 (
<div>
<FormLabel>{label}</FormLabel>
<DatePicker
datePickerType="single"
onChange={handleDateChange}
>
<DatePickerInput
id="date-picker-single"
placeholder="mm/dd/yyyy"
labelText={__('Select Date')}
value={date}
hideLabel
onChange={handleDateChange}
/>
<TimePicker
id="time-picker"
placeholder="hh:mm"
labelText={__('Select Time')}
invalid={!isValid}
invalidText="Enter a valid 12-hour time (e.g., 01:30)"
hideLabel
value={time}
onChange={handleTimeChange}
>
<TimePickerSelect
id="time-picker-select-1"
labelText={__('Select Period')}
defaultValue={period}
onChange={handlePeriodChange}
>
<SelectItem value="AM" text="AM" />
<SelectItem value="PM" text="PM" />
</TimePickerSelect>
</TimePicker>
</DatePicker>
</div>
);
};

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;
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -56,7 +75,12 @@ const EmbeddedAutomateEntryPoint = (props) => {
/>
<div className="entry-point-wrapper">
<div className="entry-point-text-input">
<TextInput id={id} type="text" labelText={__(label)} onChange={(value) => setTextValue(value.target.value)} value={textValue} />
<TextInput
id={id}
type="text"
labelText={__(label)}
value={textValue}
readOnly />
</div>
<div className="entry-point-buttons">
<div className="entry-point-open">
Expand All @@ -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
});
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ const EmbeddedWorkflowEntryPoint = (props) => {
useEffect(() => {
if (selectedValue && selectedValue.name && selectedValue.name.text) {
selectedValue.name.text = textValue;
input.onChange(selectedValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thoughts as I shared here for app/javascript/components/embedded-automate-entry-point/index.jsx

} 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 (
<div>
Expand All @@ -43,7 +48,7 @@ const EmbeddedWorkflowEntryPoint = (props) => {
) : undefined}
<div className="entry-point-wrapper">
<div className="entry-point-text-input">
<TextInput id={id} type="text" labelText={__(label)} onChange={(value) => setTextValue(value.target.value)} value={textValue} />
<TextInput id={id} type="text" labelText={__(label)} onChange={(value) => setTextValue(value.target.value)} value={textValue} readOnly />
</div>
<div className="entry-point-buttons">
<div className="entry-point-open">
Expand All @@ -61,6 +66,8 @@ const EmbeddedWorkflowEntryPoint = (props) => {
hasIconOnly
onClick={() => {
setSelectedValue({});
// Ensure the input change is triggered to update form state
input.onChange(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thoughts as I shared here for app/javascript/components/embedded-automate-entry-point/index.jsx

}}
/>
</div>
Expand Down
71 changes: 71 additions & 0 deletions app/javascript/components/service-dialog-form/data.js
Original file line number Diff line number Diff line change
@@ -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: <StringText32 /> },
{ id: 2, title: 'Text Area', icon: <TextSmallCaps32 /> },
{ id: 3, title: 'Check Box', icon: <CheckboxChecked32 /> },
{ id: 4, title: 'Dropdown', icon: <CaretDown32 /> },
{ id: 5, title: 'Radio Button', icon: <RadioButtonChecked32 /> },
{ id: 6, title: 'Datepicker', icon: <Calendar32 /> },
{ id: 7, title: 'Timepicker', icon: <Time32 /> },
{ id: 8, title: 'Tag Control', icon: <Tag32 /> },
];

/** 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);
});
};
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className="components-list-wrapper">
{
list.map((item, index) => (
<div
title={`Drag and Drop a ${item.title.toLowerCase()} to any section`}
id={item.id}
className="component-item-wrapper"
draggable="true"
onDragStart={(event) => onDragStartComponent(event, dragItems.COMPONENT)}
key={index.toString()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React uses key to track element identity and preserve state, using index as a key should be a last resort I think - see doc
Ideally, we should use a unique value from the data:

      list.map((item) => (
        <div
          key={`${item.dataKey}`}

Since we have id in the data, and if it’s unique, that can be used.

If we use index as the key and the list changes, say we delete or move a row, React might get confused about which item is which. That can lead to weird re-renders or the wrong DOM being reused.

Note: Index as keys should be fine if list won't change and items are purely presentational

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. This particular component renders a list of draggable items (see the red box in the image). The order of these items wont change. So using index as key wouldn't be a problem here.
Screenshot 2025-10-22 at 2 43 34 PM

>
<div className="component-item">
{item.icon}
{item.title}
</div>
</div>
))
}
</div>
);

DynamicComponentChooser.propTypes = {
list: PropTypes.arrayOf(PropTypes.any).isRequired,
onDragStartComponent: PropTypes.func.isRequired,
};

export default DynamicComponentChooser;
Loading
Loading