From 4f6bb6f27bd0a392e502af7f35fa03afd34a5b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Wed, 26 Feb 2025 14:04:50 +0100 Subject: [PATCH] Change: Refactor EntityComponent into a functions component with hooks Use more modern react. At the end EntityComponent should be replaced by hooks completely as it is a render props component. --- src/web/entity/Component.jsx | 248 ++++++++++--------- src/web/entity/__tests__/Component.tests.jsx | 2 +- 2 files changed, 130 insertions(+), 120 deletions(-) diff --git a/src/web/entity/Component.jsx b/src/web/entity/Component.jsx index 4123b3cdc9..b819756859 100644 --- a/src/web/entity/Component.jsx +++ b/src/web/entity/Component.jsx @@ -4,155 +4,165 @@ */ import {isDefined} from 'gmp/utils/identity'; -import React from 'react'; -import {connect} from 'react-redux'; +import {useEffect} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import useGmp from 'web/hooks/useGmp'; +import useShallowEqualSelector from 'web/hooks/useShallowEqualSelector'; import {createDeleteEntity} from 'web/store/entities/utils/actions'; import {loadUserSettingDefaults} from 'web/store/usersettings/defaults/actions'; import {getUserSettingsDefaults} from 'web/store/usersettings/defaults/selectors'; import {getUsername} from 'web/store/usersettings/selectors'; -import compose from 'web/utils/Compose'; import PropTypes from 'web/utils/PropTypes'; import {generateFilename} from 'web/utils/Render'; -import withGmp from 'web/utils/withGmp'; -class EntityComponent extends React.Component { - constructor(...args) { - super(...args); - - this.handleEntityClone = this.handleEntityClone.bind(this); - this.handleEntityDelete = this.handleEntityDelete.bind(this); - this.handleEntityDownload = this.handleEntityDownload.bind(this); - this.handleEntitySave = this.handleEntitySave.bind(this); - } - - componentDidMount() { - this.props.loadSettings(); +/** + * Creates an asynchronous action function that handles success and error callbacks. + * + * @param {Function} action - The main action function to be executed. Most of the time it should be a gmp command function. + * @param {Function} [onSuccess] - Optional callback function to be executed on successful action. + * @param {Function} [onError] - Optional callback function to be executed on action error. + * @returns {Function} - A function that takes data as an argument and executes the action with success and error handling. + */ +const createActionFunction = (action, onSuccess, onError) => async data => { + try { + const response = await action(data); + if (isDefined(onSuccess)) { + return onSuccess(response); + } + } catch (error) { + if (isDefined(onError)) { + return onError(error); + } + throw error; } + return; +}; - handleEntityDelete(entity) { - const {deleteEntity, onDeleted, onDeleteError} = this.props; - - this.handleInteraction(); +const EntityComponent2 = ({ + children, + name, + onInteraction, + onDownloaded, + onDownloadError, + onSaved, + onSaveError, + onCreated, + onCreateError, + onDeleted, + onDeleteError, + onCloned, + onCloneError, +}) => { + const gmp = useGmp(); + const username = useSelector(getUsername); + const dispatch = useDispatch(); + const cmd = gmp[name]; + const deleteEntity = entity => + dispatch(createDeleteEntity({entityType: name})(gmp)(entity.id)); + const userDefaultsSelector = useShallowEqualSelector(getUserSettingsDefaults); + const detailsExportFileName = userDefaultsSelector.getValueByName( + 'detailsexportfilename', + ); - return deleteEntity(entity.id).then(onDeleted, onDeleteError); - } + const handleInteraction = () => { + if (isDefined(onInteraction)) { + onInteraction(); + } + }; - handleEntityClone(entity) { - const {onCloned, onCloneError, gmp, name} = this.props; - const cmd = gmp[name]; + const handleEntityDownload = async entity => { + handleInteraction(); - this.handleInteraction(); + const filename = generateFilename({ + creationTime: entity.creationTime, + fileNameFormat: detailsExportFileName, + id: entity.id, + modificationTime: entity.modificationTime, + resourceName: entity.name, + resourceType: name, + username, + }); - return cmd.clone(entity).then(onCloned, onCloneError); - } + try { + const response = await cmd.export(entity); - handleEntitySave(data) { - const {gmp, name} = this.props; - const cmd = gmp[name]; + if (isDefined(onDownloaded)) { + return onDownloaded({filename, data: response.data}); + } + } catch (error) { + if (isDefined(onDownloadError)) { + return onDownloadError(error); + } + } + return; + }; - this.handleInteraction(); + const handleEntitySave = async data => { + handleInteraction(); if (isDefined(data.id)) { - const {onSaved, onSaveError} = this.props; - return cmd.save(data).then(onSaved, onSaveError); + const saveHandler = createActionFunction(cmd.save, onSaved, onSaveError); + return saveHandler(data); } - const {onCreated, onCreateError} = this.props; - return cmd.create(data).then(onCreated, onCreateError); - } + const createHandler = createActionFunction( + cmd.create, + onCreated, + onCreateError, + ); + return createHandler(data); + }; - handleEntityDownload(entity) { - const { - detailsExportFileName, - username, - gmp, - name, - onDownloaded, - onDownloadError, - } = this.props; - const cmd = gmp[name]; - - this.handleInteraction(); - - const promise = cmd.export(entity).then(response => { - const filename = generateFilename({ - creationTime: entity.creationTime, - fileNameFormat: detailsExportFileName, - id: entity.id, - modificationTime: entity.modificationTime, - resourceName: entity.name, - resourceType: name, - username, - }); - - return {filename, data: response.data}; - }); + const handleEntityDelete = async entity => { + handleInteraction(); - return promise.then(onDownloaded, onDownloadError); - } + const handler = createActionFunction( + deleteEntity, + onDeleted, + onDeleteError, + ); + return handler(entity); + }; - handleInteraction() { - const {onInteraction} = this.props; - if (isDefined(onInteraction)) { - onInteraction(); - } - } + const handleEntityClone = async entity => { + handleInteraction(); - render() { - const {children} = this.props; + const handler = createActionFunction(cmd.clone, onCloned, onCloneError); + return handler(entity); + }; - return children({ - create: this.handleEntitySave, - clone: this.handleEntityClone, - delete: this.handleEntityDelete, - save: this.handleEntitySave, - download: this.handleEntityDownload, - }); - } -} + useEffect(() => { + const loadSettings = () => dispatch(loadUserSettingDefaults(gmp)()); + if ( + !userDefaultsSelector.isLoading() && + !isDefined(detailsExportFileName) && + !isDefined(userDefaultsSelector.getError()) + ) { + loadSettings(); + } + }, [detailsExportFileName, dispatch, gmp, userDefaultsSelector]); + + return children({ + create: handleEntitySave, + clone: handleEntityClone, + delete: handleEntityDelete, + save: handleEntitySave, + download: handleEntityDownload, + }); +}; -EntityComponent.propTypes = { +EntityComponent2.propTypes = { children: PropTypes.func.isRequired, - deleteEntity: PropTypes.func.isRequired, - detailsExportFileName: PropTypes.string, - gmp: PropTypes.gmp.isRequired, - loadSettings: PropTypes.func.isRequired, name: PropTypes.string.isRequired, - username: PropTypes.string, - onCloneError: PropTypes.func, onCloned: PropTypes.func, - onCreateError: PropTypes.func, + onCloneError: PropTypes.func, onCreated: PropTypes.func, - onDeleteError: PropTypes.func, + onCreateError: PropTypes.func, onDeleted: PropTypes.func, - onDownloadError: PropTypes.func, + onDeleteError: PropTypes.func, onDownloaded: PropTypes.func, - onInteraction: PropTypes.func.isRequired, - onSaveError: PropTypes.func, - onSaved: PropTypes.func, -}; - -const mapStateToProps = rootState => { - const userDefaultsSelector = getUserSettingsDefaults(rootState); - const username = getUsername(rootState); - const detailsExportFileName = userDefaultsSelector.getValueByName( - 'detailsexportfilename', - ); - return { - detailsExportFileName, - username, - }; -}; - -const mapDispatchToProps = (dispatch, {name, gmp}) => { - const deleteEntity = createDeleteEntity({entityType: name}); - return { - deleteEntity: id => dispatch(deleteEntity(gmp)(id)), - loadSettings: () => dispatch(loadUserSettingDefaults(gmp)()), - }; + onDownloadError: PropTypes.func, + onInteraction: PropTypes.func, }; -export default compose( - withGmp, - connect(mapStateToProps, mapDispatchToProps), -)(EntityComponent); +export default EntityComponent2; diff --git a/src/web/entity/__tests__/Component.tests.jsx b/src/web/entity/__tests__/Component.tests.jsx index a8a7c266a0..cb8b2925e7 100644 --- a/src/web/entity/__tests__/Component.tests.jsx +++ b/src/web/entity/__tests__/Component.tests.jsx @@ -320,8 +320,8 @@ describe('EntityComponent', () => { )} , ); - await wait(); // wait for currentSettings to be resolved and put into the store + expect(currentSettings).toHaveBeenCalledOnce(); queryByTestId('button').click(); await wait(); expect(onDownloaded).toHaveBeenCalledWith({