Skip to content

Commit

Permalink
Change: Refactor EntityComponent into a functions component with hooks
Browse files Browse the repository at this point in the history
Use more modern react. At the end EntityComponent should be replaced by
hooks completely as it is a render props component.
  • Loading branch information
bjoernricks committed Feb 26, 2025
1 parent 4a93625 commit e7dc65f
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 119 deletions.
246 changes: 128 additions & 118 deletions src/web/entity/Component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 EntityComponent = ({
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 = {
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 EntityComponent;
2 changes: 1 addition & 1 deletion src/web/entity/__tests__/Component.tests.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,8 @@ describe('EntityComponent', () => {
)}
</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({
Expand Down

0 comments on commit e7dc65f

Please sign in to comment.