-
Notifications
You must be signed in to change notification settings - 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
962c4cc
31f4c05
8c55873
fa64667
c99197c
90804e4
9d2aec4
2278783
896ab0a
11f6b48
33c6798
70d454b
2bdf207
78be4a5
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,44 @@ | ||
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} setMessage - Callback for handling close button clicks. | ||
* @param {boolean} showCloseButton - Whether to display the close button. | ||
*/ | ||
const InlineFlashMessage = ({ message, setMessage, 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={setMessage} | ||
/> | ||
); | ||
}; | ||
|
||
InlineFlashMessage.propTypes = { | ||
message: PropTypes.shape({ | ||
kind: PropTypes.oneOf(['success', 'error', 'info', 'warning']), | ||
title: PropTypes.string, | ||
subtitle: PropTypes.string, | ||
}), | ||
setMessage: PropTypes.func, | ||
showCloseButton: PropTypes.bool, | ||
}; | ||
|
||
InlineFlashMessage.defaultProps = { | ||
message: 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. since we are doing |
||
setMessage: () => {}, | ||
showCloseButton: true, | ||
}; | ||
|
||
export default InlineFlashMessage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import React, { useState } from 'react'; | ||
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 = (field) => { | ||
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. Correct me here if I am wrong
So I was thinking - just like we did with InlineFlashMessage, inline destructuring of component props is generally cleaner instead of accessing them via and then add props validations and default props for them:
Note: date, time and period having an empty string as default value ( |
||
const { initialData, onChange } = field; | ||
|
||
const [date, setDate] = useState(initialData.date || getCurrentDate); | ||
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. I guess it would be better to call the function immediately ( also include the lazy initialization |
||
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 combinedDateTime = () => { | ||
const dateTime = `${date} ${time} ${period}`; | ||
return dateTime; | ||
}; | ||
|
||
const handleDateChange = (newDate) => { | ||
if (newDate.length > 0) { | ||
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.
|
||
const formattedDate = new Intl.DateTimeFormat('en-US', { | ||
month: '2-digit', | ||
day: '2-digit', | ||
year: 'numeric', | ||
}).format(newDate[0]); | ||
setDate(formattedDate); | ||
onChange({ value: combinedDateTime(), initialData }); | ||
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. Like discussed below, due to batched state updates we might want to update
and then from
|
||
} | ||
}; | ||
|
||
// 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 | ||
setIsValid(timeRegex.test(value)); | ||
}; | ||
|
||
const handleTimeChange = (event) => { | ||
const newTime = event.target.value; | ||
setTime(newTime); | ||
validateTime(newTime); | ||
if (isValid) onChange({ value: combinedDateTime(), initialData }); | ||
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. Tried "11:5" in the time field, which doesn't match our pattern, so it's treated as an invalid time, the regex correctly returns false, which we also assign to the Returning the regex result from
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. The Instead of relying on stale state we could update
🔝 we can also avoid the intermediate variable( Now from
So that correct updated time is returned from |
||
}; | ||
|
||
const handlePeriodChange = (event) => { | ||
setPeriod(event.target.value); | ||
onChange({ value: combinedDateTime(), initialData }); | ||
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 as above, due to batched state updates we might want to update
and then from
|
||
}; | ||
|
||
return ( | ||
<div> | ||
<FormLabel>{field.label}</FormLabel> | ||
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. |
||
<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> | ||
); | ||
}; | ||
|
||
export default CustomDateTimePicker; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
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 returens 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, onSuccess) => { | ||
const payload = formattedCatalogPayload(data); | ||
|
||
API.post('/api/service_dialogs', payload, { | ||
skipErrors: [400], | ||
}).then(() => { | ||
// Redirect to the service dialogs explorer page after successful save | ||
if (typeof onSuccess === 'function') { | ||
onSuccess(); | ||
} else { | ||
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()} | ||
> | ||
<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.
I think in React-17+, JSX works without importing React, so we should be able to drop it post our upgrade 👍