-
Couldn't load subscription status.
- Fork 365
Conversion of Service Dialogs Form from Angular to React #9592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
90c4a97
078435b
89161ef
5cbf8d7
0e70609
e579fc7
527f929
d5dd3b4
703cb11
def500a
2e80de6
16454f0
dfca0a7
4f8d1b7
ed1acfc
f875f84
88e730e
3e276eb
ce25cd2
93d7b93
cc97620
842d3c3
0ecf046
d126ed3
2842dee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import React from 'react'; | ||
elsamaryv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| 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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 No issues with this implementation, this should work well once we apply zero-padding to the hour value in |
||
| 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 |
|---|---|---|
|
|
@@ -26,9 +26,14 @@ const EmbeddedWorkflowEntryPoint = (props) => { | |
| useEffect(() => { | ||
| if (selectedValue && selectedValue.name && selectedValue.name.text) { | ||
| selectedValue.name.text = textValue; | ||
| input.onChange(selectedValue); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thoughts as I shared here for |
||
| } 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> | ||
|
|
@@ -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"> | ||
|
|
@@ -61,6 +66,8 @@ const EmbeddedWorkflowEntryPoint = (props) => { | |
| hasIconOnly | ||
| onClick={() => { | ||
| setSelectedValue({}); | ||
| // Ensure the input change is triggered to update form state | ||
| input.onChange(null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thoughts as I shared here for |
||
| }} | ||
| /> | ||
| </div> | ||
|
|
||
| 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()} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Since we have 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| > | ||
| <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; | ||




There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overflow-x: scrollmight not be great on modals, it can mess with alignment when scrollbars show up unnecessarily.I think
overflow-x: autowould work better here.Also, do we have a size prop for modals? Something like
size="xl"(like bootstrap-modal does) to make it wider?