-
Notifications
You must be signed in to change notification settings - Fork 303
Fixes #38900 - Convert Host Collections list/details page to React #11563
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 4 commits
a6d2de0
8753773
21ed801
1491051
63e8fa4
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,24 @@ | ||
| .inline-edit-display { | ||
| align-items: center; | ||
|
|
||
| .inline-edit-value { | ||
| display: block; | ||
| min-height: 1.5rem; | ||
| } | ||
|
|
||
| button { | ||
| visibility: hidden; | ||
| } | ||
|
|
||
| &:hover button { | ||
| visibility: visible; | ||
| } | ||
| } | ||
|
|
||
| .inline-edit-form { | ||
| margin-bottom: 0; | ||
|
|
||
| .pf-c-form__group-control { | ||
| width: 100%; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import React, { useState } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { translate as __ } from 'foremanReact/common/I18n'; | ||
| import { | ||
| Button, | ||
| Flex, | ||
| FlexItem, | ||
| FormGroup, | ||
| FormSelect, | ||
| FormSelectOption, | ||
| TextArea, | ||
| TextInput, | ||
| } from '@patternfly/react-core'; | ||
| import { CheckIcon, TimesIcon, PencilAltIcon } from '@patternfly/react-icons'; | ||
| import './InlineEdit.scss'; | ||
|
|
||
| const InlineEdit = ({ | ||
| value, onSave, isRequired, multiline, type, options, | ||
| }) => { | ||
| const [isEditing, setIsEditing] = useState(false); | ||
| const [editValue, setEditValue] = useState(value); | ||
|
|
||
| const handleSave = () => { | ||
| if (isRequired && !editValue.trim()) { | ||
| return; | ||
| } | ||
| onSave(editValue); | ||
| setIsEditing(false); | ||
| }; | ||
|
|
||
| const handleCancel = () => { | ||
| setEditValue(value); | ||
| setIsEditing(false); | ||
| }; | ||
|
|
||
| const handleKeyDown = (e) => { | ||
| if (e.key === 'Enter' && !multiline) { | ||
| e.preventDefault(); | ||
| handleSave(); | ||
| } else if (e.key === 'Escape') { | ||
| handleCancel(); | ||
| } | ||
| }; | ||
|
|
||
| const renderValue = () => { | ||
| if (type === 'select') { | ||
| const option = options.find(opt => opt.value === value); | ||
| return option?.label || value || __('Not set'); | ||
| } | ||
| return value || __('Not set'); | ||
| }; | ||
|
|
||
| const renderEditField = () => { | ||
| if (type === 'select') { | ||
| return ( | ||
| <FormSelect | ||
| value={editValue} | ||
| onChange={newValue => setEditValue(newValue)} | ||
| aria-label="inline-edit-select" | ||
| ouiaId="inline-edit-select" | ||
| > | ||
| {options.map(option => ( | ||
| <FormSelectOption | ||
| key={option.value} | ||
| value={option.value} | ||
| label={option.label} | ||
| /> | ||
| ))} | ||
| </FormSelect> | ||
| ); | ||
| } | ||
|
|
||
| if (multiline) { | ||
| return ( | ||
| <TextArea | ||
| value={editValue} | ||
| onChange={(event, newValue) => setEditValue(newValue)} | ||
| onKeyDown={handleKeyDown} | ||
| aria-label="inline-edit-textarea" | ||
| autoFocus | ||
| rows={3} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <TextInput | ||
| value={editValue} | ||
| onChange={(event, newValue) => setEditValue(newValue)} | ||
| onKeyDown={handleKeyDown} | ||
| aria-label="inline-edit-input" | ||
| autoFocus | ||
| isRequired={isRequired} | ||
| ouiaId="inline-edit-input" | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| if (!isEditing) { | ||
| return ( | ||
| <Flex className="inline-edit-display"> | ||
| <FlexItem flex={{ default: 'flex_1' }}> | ||
| <span className="inline-edit-value">{renderValue()}</span> | ||
| </FlexItem> | ||
| <FlexItem> | ||
| <Button | ||
| variant="plain" | ||
| aria-label="Edit" | ||
| onClick={() => { | ||
| setEditValue(value); | ||
| setIsEditing(true); | ||
| }} | ||
| icon={<PencilAltIcon />} | ||
| ouiaId="inline-edit-edit-button" | ||
| /> | ||
| </FlexItem> | ||
| </Flex> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <FormGroup className="inline-edit-form"> | ||
| <Flex> | ||
| <FlexItem flex={{ default: 'flex_1' }}> | ||
| {renderEditField()} | ||
| </FlexItem> | ||
| <FlexItem> | ||
| <Button | ||
| variant="plain" | ||
| aria-label="Save" | ||
| onClick={handleSave} | ||
| icon={<CheckIcon />} | ||
| isDisabled={isRequired && !editValue.trim()} | ||
| ouiaId="inline-edit-save-button" | ||
| /> | ||
| </FlexItem> | ||
| <FlexItem> | ||
| <Button | ||
| variant="plain" | ||
| aria-label="Cancel" | ||
| onClick={handleCancel} | ||
| icon={<TimesIcon />} | ||
| ouiaId="inline-edit-cancel-button" | ||
| /> | ||
| </FlexItem> | ||
| </Flex> | ||
| </FormGroup> | ||
| ); | ||
| }; | ||
|
|
||
| InlineEdit.propTypes = { | ||
| value: PropTypes.string, | ||
| onSave: PropTypes.func.isRequired, | ||
| isRequired: PropTypes.bool, | ||
| multiline: PropTypes.bool, | ||
| type: PropTypes.oneOf(['text', 'select']), | ||
| options: PropTypes.arrayOf(PropTypes.shape({ | ||
| value: PropTypes.string, | ||
| label: PropTypes.string, | ||
| })), | ||
| }; | ||
|
|
||
| InlineEdit.defaultProps = { | ||
| value: '', | ||
| isRequired: false, | ||
| multiline: false, | ||
| type: 'text', | ||
| options: [], | ||
| }; | ||
|
|
||
| export default InlineEdit; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,117 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useDispatch } from 'react-redux'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import PropTypes from 'prop-types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { translate as __ } from 'foremanReact/common/I18n'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Modal, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ModalVariant, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Form, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FormGroup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TextInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ActionGroup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Button, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@patternfly/react-core'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { addToast } from 'foremanReact/components/ToastsList/slice'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { copyHostCollection } from '../HostCollectionsActions'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CopyHostCollectionModal = ({ isOpen, onClose, hostCollection }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dispatch = useDispatch(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [newName, setNewName] = useState(`Copy of ${hostCollection?.name || ''}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [copying, setCopying] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSuccess = (data) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setCopying(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dispatch(addToast({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'success', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: __('Host collection copied successfully'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = `/labs/host_collections/${data.id}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+30
Contributor
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. suggestion: Directly setting window.location.href for navigation may bypass SPA routing. Use the router's navigation method (such as history.push) to maintain SPA behavior and prevent unnecessary page reloads.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleError = (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
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. This seems like wheel-reinvention too. Tell it to look in webpack/scenes/Tasks/helpers.js and reuse stuff from there. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setCopying(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorMsg = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error?.response?.data?.error?.full_messages?.[0] || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error?.response?.data?.displayMessage || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __('Failed to copy host collection'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dispatch(addToast({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'error', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: errorMsg, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onCopy = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setCopying(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dispatch(copyHostCollection(hostCollection.id, newName, handleSuccess, handleError)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const submitDisabled = !newName?.trim().length || copying; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Modal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ouiaId="copy-host-collection-modal" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={__('Copy host collection')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant={ModalVariant.small} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen={isOpen} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appendTo={document.body} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Form | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCopy(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <FormGroup label={__('Name')} isRequired fieldId="new-name"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <TextInput | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isRequired | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="new-name" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="new-name" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ouiaId="new-name-input" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="new-name" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={newName} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(_event, value) => setNewName(value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDisabled={copying} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </FormGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ActionGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ouiaId="copy-button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-label="copy" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="primary" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDisabled={submitDisabled} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading={copying} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {__('Copy')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ouiaId="cancel-button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="link" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={onClose} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isDisabled={copying} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {__('Cancel')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ActionGroup> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Modal> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CopyHostCollectionModal.propTypes = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen: PropTypes.bool, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose: PropTypes.func.isRequired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostCollection: PropTypes.shape({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: PropTypes.string.isRequired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).isRequired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CopyHostCollectionModal.defaultProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOpen: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default CopyHostCollectionModal; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
Katello already has an
ActionableDetailcomponent (used in ContentViewInfo) that does this same thing. Make sure to tell your AI to inspect the Katello code base and follow existing patterns - it likes to default to reinventing the wheel.