From 04bf199e58cab1f7f6df17217c900f9078f17670 Mon Sep 17 00:00:00 2001 From: Lukas Jezek Date: Fri, 30 Jan 2026 15:58:08 +0000 Subject: [PATCH] Fixes #39047 - replace useForemanModal in BulkActions --- .../extensions/Hosts/ActionsBar/index.js | 45 +++++----------- .../BulkAssignCVEnvsModal/index.js | 11 ++-- .../BulkChangeHostCollectionsModal/index.js | 12 ++--- .../BulkErrataWizard/BulkErrataWizard.js | 10 ++-- .../BulkActions/BulkErrataWizard/index.js | 6 +-- .../BulkManageTracesModal/index.js | 8 +-- .../BulkPackagesWizard/BulkPackagesWizard.js | 4 +- .../BulkActions/BulkPackagesWizard/index.js | 4 +- .../BulkRepositorySetsWizard.js | 4 +- .../BulkRepositorySetsWizard/index.js | 4 +- .../BulkSystemPurposeModal/index.js | 8 +-- .../Hosts/BulkActions/bulkModalState.js | 51 +++++++++++++++++++ 12 files changed, 103 insertions(+), 64 deletions(-) create mode 100644 webpack/components/extensions/Hosts/BulkActions/bulkModalState.js diff --git a/webpack/components/extensions/Hosts/ActionsBar/index.js b/webpack/components/extensions/Hosts/ActionsBar/index.js index aa85dea637e..dbb95648f77 100644 --- a/webpack/components/extensions/Hosts/ActionsBar/index.js +++ b/webpack/components/extensions/Hosts/ActionsBar/index.js @@ -1,15 +1,14 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; -import { useDispatch } from 'react-redux'; import { Menu, MenuItem, MenuContent, MenuList } from '@patternfly/react-core'; import { BanIcon } from '@patternfly/react-icons'; import { translate as __ } from 'foremanReact/common/I18n'; import { foremanUrl } from 'foremanReact/common/helpers'; import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; -import { addModal } from 'foremanReact/components/ForemanModal/ForemanModalActions'; import { useForemanOrganization, useForemanContext } from 'foremanReact/Root/Context/ForemanContext'; import './ActionsBar.scss'; +import { openBulkModal } from '../BulkActions/bulkModalState'; + const DisabledMenuItemDescription = ({ disabledReason }) => ( @@ -34,27 +33,6 @@ const HostActionsBar = () => { setMenuOpen, } = useContext(ForemanHostsIndexActionsBarContext); - const dispatch = useDispatch(); - useEffect(() => { - [ - 'bulk-assign-cves-modal', - 'bulk-packages-wizard', - 'bulk-errata-wizard', - 'bulk-repo-sets-wizard', - 'bulk-change-host-collections-modal', - 'bulk-system-purpose-modal', - 'bulk-manage-traces-modal', - ].forEach((id) => { - dispatch(addModal({ id })); - }); - }, [dispatch]); - const { setModalOpen: openBulkAssignCVEnvsModal } = useForemanModal({ id: 'bulk-assign-cves-modal' }); - const { setModalOpen: openBulkPackagesWizardModal } = useForemanModal({ id: 'bulk-packages-wizard' }); - const { setModalOpen: openBulkErrataWizardModal } = useForemanModal({ id: 'bulk-errata-wizard' }); - const { setModalOpen: openBulkRepositorySetsWizardModal } = useForemanModal({ id: 'bulk-repo-sets-wizard' }); - const { setModalOpen: openBulkSystemPurposeModal } = useForemanModal({ id: 'bulk-system-purpose-modal' }); - const { setModalOpen: openBulkManageTracesModal } = useForemanModal({ id: 'bulk-manage-traces-modal' }); - const orgId = useForemanOrganization()?.id; const foremanContext = useForemanContext(); const allowMultipleContentViews = @@ -68,6 +46,11 @@ const HostActionsBar = () => { href = foremanUrl(`/change_host_content_source?search=${fetchBulkParams()}`); } + const handleOpenBulkModal = (modalId) => { + setMenuOpen(false); + setTimeout(() => openBulkModal(modalId, true), 0); + }; + return ( <> { handleOpenBulkModal('bulk-packages-wizard')} isDisabled={selectedCount === 0 || !orgId} description={!orgId && } > @@ -90,7 +73,7 @@ const HostActionsBar = () => { handleOpenBulkModal('bulk-errata-wizard')} isDisabled={selectedCount === 0} > {__('Errata')} @@ -98,7 +81,7 @@ const HostActionsBar = () => { handleOpenBulkModal('bulk-repo-sets-wizard')} isDisabled={selectedCount === 0 || !orgId} description={!orgId && } > @@ -115,7 +98,7 @@ const HostActionsBar = () => { handleOpenBulkModal('bulk-assign-cves-modal')} isDisabled={selectedCount === 0 || !orgId} description={!orgId && } > @@ -124,7 +107,7 @@ const HostActionsBar = () => { handleOpenBulkModal('bulk-system-purpose-modal')} isDisabled={selectedCount === 0 || !orgId} description={!orgId && } > @@ -140,7 +123,7 @@ const HostActionsBar = () => { handleOpenBulkModal('bulk-manage-traces-modal')} isDisabled={selectedCount === 0 || !orgId} description={!orgId && } > diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/index.js index 4b09ff1bfec..eb9eb54f822 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkAssignCVEnvsModal/index.js @@ -1,16 +1,17 @@ import React, { useContext } from 'react'; import { useForemanOrganization, useForemanContext } from 'foremanReact/Root/Context/ForemanContext'; import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; +import { useBulkModalOpen } from '../bulkModalState'; import BulkAssignCVEnvsModal from './BulkAssignCVEnvsModal'; const BulkAssignCVEnvsModalScene = () => { const orgId = useForemanOrganization()?.id; const { selectedCount, fetchBulkParams } = useContext(ForemanActionsBarContext); - const { modalOpen, setModalClosed } = useForemanModal({ id: 'bulk-assign-cves-modal' }); + const { isOpen, close: closeModal } = useBulkModalOpen('bulk-assign-cves-modal'); const foremanContext = useForemanContext(); const allowMultipleContentViews = - foremanContext?.metadata?.katello?.allow_multiple_content_views ?? true; + foremanContext?.metadata?.katello?.allow_multiple_content_views ?? true; + if (!orgId) return null; @@ -19,8 +20,8 @@ const BulkAssignCVEnvsModalScene = () => { key="bulk-assign-cves-modal" selectedCount={selectedCount} fetchBulkParams={fetchBulkParams} - isOpen={modalOpen} - closeModal={setModalClosed} + isOpen={isOpen} + closeModal={closeModal} orgId={orgId} allowMultipleContentViews={allowMultipleContentViews} /> diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/index.js index c350aa4eae4..051bcc39184 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCollectionsModal/index.js @@ -5,8 +5,8 @@ import { BanIcon } from '@patternfly/react-icons'; import { translate as __ } from 'foremanReact/common/I18n'; import { useForemanOrganization } from 'foremanReact/Root/Context/ForemanContext'; import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import BulkChangeHostCollectionsModal from './BulkChangeHostCollectionsModal'; +import { openBulkModal, useBulkModalOpen } from '../bulkModalState'; const DisabledMenuItemDescription = ({ disabledReason }) => ( @@ -26,13 +26,13 @@ DisabledMenuItemDescription.propTypes = { // This component renders only the MenuItem trigger (for the slot in the menu) export const BulkChangeHostCollectionsMenuItem = ({ selectedCount }) => { const orgId = useForemanOrganization()?.id; - const { setModalOpen } = useForemanModal({ id: 'bulk-change-host-collections-modal' }); + const openModal = () => openBulkModal('bulk-change-host-collections-modal', true); return ( } > @@ -53,7 +53,7 @@ BulkChangeHostCollectionsMenuItem.defaultProps = { const BulkChangeHostCollectionsModalScene = () => { const orgId = useForemanOrganization()?.id; const { selectedCount, fetchBulkParams } = useContext(ForemanActionsBarContext); - const { modalOpen, setModalClosed } = useForemanModal({ id: 'bulk-change-host-collections-modal' }); + const { isOpen, close: closeModal } = useBulkModalOpen('bulk-change-host-collections-modal'); if (!orgId) return null; @@ -62,8 +62,8 @@ const BulkChangeHostCollectionsModalScene = () => { key="bulk-change-host-collections-modal" fetchBulkParams={fetchBulkParams} selectedCount={selectedCount} - isOpen={modalOpen} - closeModal={setModalClosed} + isOpen={isOpen} + closeModal={closeModal} /> ); }; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js index fcfc0ac7250..a988b2ef9b9 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/BulkErrataWizard.js @@ -1,11 +1,11 @@ import React, { useState, createContext, useContext } from 'react'; +import PropTypes from 'prop-types'; import { Wizard, WizardHeader, WizardStep, } from '@patternfly/react-core'; import { translate as __ } from 'foremanReact/common/I18n'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; import { useTableIndexAPIResponse } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks'; @@ -53,8 +53,7 @@ export const useErrataHostsBulkSelect = ({ initialSelectedHosts, modalIsOpen }) export const ERRATA_URL = `${katelloApi.getApiUrl('/errata')}?per_page=7&include_permissions=true&errata_restrict_installable=true`; -const BulkErrataWizard = () => { - const { modalOpen, setModalClosed: closeModal } = useForemanModal({ id: 'bulk-errata-wizard' }); +const BulkErrataWizard = ({ isOpen: modalOpen, closeModal }) => { const { selectedCount: initialSelectedHostCount, fetchBulkParams } = useContext(ForemanActionsBarContext); @@ -159,3 +158,8 @@ const BulkErrataWizard = () => { }; export default BulkErrataWizard; + +BulkErrataWizard.propTypes = { + isOpen: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired, +}; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js index 779979f9f00..ca3e7ebc5e9 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkErrataWizard/index.js @@ -1,10 +1,10 @@ import React from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import BulkErrataWizard from './BulkErrataWizard'; +import { useBulkModalOpen } from '../bulkModalState'; const BulkErrataWizardModal = () => { - const { modalOpen: isOpen } = useForemanModal({ id: 'bulk-errata-wizard' }); + const { isOpen, close: closeModal } = useBulkModalOpen('bulk-errata-wizard'); return ( { hasNoBodyWrapper variant={ModalVariant.medium} > - + ); }; diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/index.js index 90b179d646d..86ac76d9994 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkManageTracesModal/index.js @@ -1,14 +1,14 @@ import React, { useContext } from 'react'; import { useForemanOrganization } from 'foremanReact/Root/Context/ForemanContext'; import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; +import { useBulkModalOpen } from '../bulkModalState'; import BulkManageTracesModal from './BulkManageTracesModal'; const BulkManageTracesModalScene = () => { const orgId = useForemanOrganization()?.id; const contextValue = useContext(ForemanActionsBarContext); const { selectedCount, fetchBulkParams } = contextValue || {}; - const { modalOpen, setModalClosed } = useForemanModal({ id: 'bulk-manage-traces-modal' }); + const { isOpen, close: closeModal } = useBulkModalOpen('bulk-manage-traces-modal'); if (!orgId) return null; @@ -22,8 +22,8 @@ const BulkManageTracesModalScene = () => { key="bulk-manage-traces-modal" selectedCount={selectedCount || 0} fetchBulkParams={fetchBulkParams} - isOpen={modalOpen} - closeModal={setModalClosed} + isOpen={isOpen} + closeModal={closeModal} orgId={orgId} /> ); diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js index 775842c7ed8..359a2d5a870 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/BulkPackagesWizard.js @@ -11,7 +11,6 @@ import { } from '@patternfly/react-core'; import { translate as __ } from 'foremanReact/common/I18n'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import { useBulkSelect } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks'; import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar'; import { useTableIndexAPIResponse } from 'foremanReact/components/PF4/TableIndexPage/Table/TableIndexHooks'; @@ -23,6 +22,7 @@ import { BulkPackagesUpgradeTable, BulkPackagesInstallTable, BulkPackagesRemoveT import { BulkPackagesReviewFooter } from './04_ReviewFooter'; import katelloApi, { foremanApi } from '../../../../../services/api'; import PACKAGE_CONTENT_TYPE_NAMES from '../BulkActionsConstants'; +import { useBulkModalOpen } from '../bulkModalState'; export const UPGRADE_ALL = 'upgradeAll'; export const UPGRADE = 'upgrade'; @@ -73,7 +73,7 @@ export const getPackagesUrl = (selectedAction, contentTypeName) => { }; const BulkPackagesWizard = () => { - const { modalOpen, setModalClosed: closeModal } = useForemanModal({ id: 'bulk-packages-wizard' }); + const { isOpen: modalOpen, close: closeModal } = useBulkModalOpen('bulk-packages-wizard'); const [selectedAction, setSelectedAction] = useState(UPGRADE_ALL); diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/index.js index 75dc0381092..d389cbd8b81 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkPackagesWizard/index.js @@ -1,10 +1,10 @@ import React from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import BulkPackagesWizard from './BulkPackagesWizard'; +import { useBulkModalOpen } from '../bulkModalState'; const BulkPackagesWizardModal = () => { - const { modalOpen: isOpen } = useForemanModal({ id: 'bulk-packages-wizard' }); + const { isOpen } = useBulkModalOpen('bulk-packages-wizard'); return ( katelloApi.getApiUrl(`/repository_sets?per_page=${DEFAULT_PER_PAGE}&include_permissions=true&enabled=true&with_custom=true&organization_id=${orgId}`); const BulkRepositorySetsWizard = () => { - const { modalOpen, setModalClosed: closeModal } = useForemanModal({ id: 'bulk-repo-sets-wizard' }); + const { isOpen: modalOpen, close: closeModal } = useBulkModalOpen('bulk-repo-sets-wizard'); const orgId = useForemanOrganization()?.id; const [pendingOverrides, setPendingOverrides] = useState({}); // { repo_label: 1 } const [shouldValidateStep2, setShouldValidateStep2] = useState(false); diff --git a/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/index.js b/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/index.js index f2bba331914..597a71caefc 100644 --- a/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/index.js +++ b/webpack/components/extensions/Hosts/BulkActions/BulkRepositorySetsWizard/index.js @@ -1,10 +1,10 @@ import React from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core'; -import { useForemanModal } from 'foremanReact/components/ForemanModal/ForemanModalHooks'; import BulkRepositorySetsWizard from './BulkRepositorySetsWizard'; +import { useBulkModalOpen } from '../bulkModalState'; const BulkRepositorySetsWizardModal = () => { - const { modalOpen: isOpen } = useForemanModal({ id: 'bulk-repo-sets-wizard' }); + const { isOpen } = useBulkModalOpen('bulk-repo-sets-wizard'); return ( { const orgId = useForemanOrganization()?.id; const { selectedCount, fetchBulkParams } = useContext(ForemanActionsBarContext); - const { modalOpen, setModalClosed } = useForemanModal({ id: 'bulk-system-purpose-modal' }); + const { isOpen, close: closeModal } = useBulkModalOpen('bulk-system-purpose-modal'); if (!orgId) return null; @@ -16,8 +16,8 @@ const BulkSystemPurposeModalScene = () => { key="bulk-system-purpose-modal" selectedCount={selectedCount} fetchBulkParams={fetchBulkParams} - isOpen={modalOpen} - closeModal={setModalClosed} + isOpen={isOpen} + closeModal={closeModal} orgId={orgId} /> ); diff --git a/webpack/components/extensions/Hosts/BulkActions/bulkModalState.js b/webpack/components/extensions/Hosts/BulkActions/bulkModalState.js new file mode 100644 index 00000000000..c950973df03 --- /dev/null +++ b/webpack/components/extensions/Hosts/BulkActions/bulkModalState.js @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; + +// Global states for storing modal open statuses and their listeners +const modalState = {}; + +// Object of React state setters ex: +// { +// "bulk-assign-cves-modal": [setIsOpen], +// "bulk-packages-wizard": [setIsOpen], +// } +const modalStateOpeners = {}; + +export const openBulkModal = (id, isOpen) => { + // Avoid unnecessary updates + if (modalState[id] === isOpen) return; + + if (isOpen) modalState[id] = true; + else delete modalState[id]; + + (modalStateOpeners[id] || []).forEach(listener => listener(isOpen)); +}; + +export const useBulkModalOpen = (id) => { + const [isOpen, setIsOpen] = useState(() => Boolean(modalState[id])); + + useEffect(() => { + if (!modalStateOpeners[id]) modalStateOpeners[id] = []; + + // Prevent duplicate subscriptions + if (!modalStateOpeners[id].includes(setIsOpen)) { + modalStateOpeners[id].push(setIsOpen); + } + + return () => { + modalStateOpeners[id] = modalStateOpeners[id].filter(l => l !== setIsOpen); + + // Cleanup empty listener arrays + if (modalStateOpeners[id].length === 0) { + delete modalStateOpeners[id]; + } + }; + }, [id]); + + // Return with helper functions + return { + isOpen, + open: () => openBulkModal(id, true), + close: () => openBulkModal(id, false), + toggle: () => openBulkModal(id, !isOpen), + }; +};