Skip to content

Commit

Permalink
Add: Extract a useEntityClone hook from EntityComponent
Browse files Browse the repository at this point in the history
EntityComponent is a render prop component and can be replaced by
several hooks.
  • Loading branch information
bjoernricks committed Mar 10, 2025
1 parent 75b9024 commit 434e357
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 24 deletions.
30 changes: 6 additions & 24 deletions src/web/entity/EntityComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {isDefined} from 'gmp/utils/identity';
import useEntityClone from 'web/entity/hooks/useEntityClone';
import useEntityDelete from 'web/entity/hooks/useEntityDelete';
import useEntityDownload from 'web/entity/hooks/useEntityDownload';
import useEntitySave from 'web/entity/hooks/useEntitySave';
import {actionFunction} from 'web/entity/hooks/utils';
import useGmp from 'web/hooks/useGmp';
import useTranslation from 'web/hooks/useTranslation';
import PropTypes from 'web/utils/PropTypes';

const EntityComponent = ({
Expand All @@ -27,16 +24,6 @@ const EntityComponent = ({
onCloned,
onCloneError,
}) => {
const gmp = useGmp();
const [_] = useTranslation();
const cmd = gmp[name];

const handleInteraction = () => {
if (isDefined(onInteraction)) {
onInteraction();
}
};

const handleEntityDownload = useEntityDownload(name, {
onDownloadError,
onDownloaded,
Expand All @@ -57,16 +44,11 @@ const EntityComponent = ({
onInteraction,
});

const handleEntityClone = async entity => {
handleInteraction();

return actionFunction(
cmd.clone(entity),
onCloned,
onCloneError,
_('{{name}} cloned successfully.', {name: entity.name}),
);
};
const handleEntityClone = useEntityClone(name, {
onCloneError,
onCloned,
onInteraction,
});

return children({
create: handleEntitySave,
Expand Down
64 changes: 64 additions & 0 deletions src/web/entity/hooks/__tests__/usEntityClone.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* SPDX-FileCopyrightText: 2025 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect, testing} from '@gsa/testing';
import useEntityClone from 'web/entity/hooks/useEntityClone';
import {rendererWith, wait} from 'web/utils/Testing';

describe('useEntityClone', () => {
test('should allow to clone an entity', async () => {
const entity = {id: '123'};
const cloneEntity = testing.fn().mockResolvedValue(entity);
const gmp = {
foo: {clone: cloneEntity},
};
const onCloned = testing.fn();
const onCloneError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntityClone('foo', {
onCloned,
onCloneError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(cloneEntity).toHaveBeenCalledWith(entity);
expect(onCloned).toHaveBeenCalledWith(entity);
expect(onCloneError).not.toHaveBeenCalled();
expect(onInteraction).toHaveBeenCalledOnce();
});

test('should call onCloneError when cloning an entity fails', async () => {
const cloneEntity = testing.fn().mockRejectedValue(new Error('error'));
const entity = {id: '123'};
const gmp = {
foo: {clone: cloneEntity},
};
const onCloned = testing.fn();
const onCloneError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntityClone('foo', {
onCloned,
onCloneError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(cloneEntity).toHaveBeenCalledWith(entity);
expect(onCloned).not.toHaveBeenCalled();
expect(onCloneError).toHaveBeenCalledWith(new Error('error'));
expect(onInteraction).toHaveBeenCalledOnce();
});
});
45 changes: 45 additions & 0 deletions src/web/entity/hooks/useEntityClone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2025 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {isDefined} from 'gmp/utils/identity';
import {actionFunction} from 'web/entity/hooks/utils';
import useGmp from 'web/hooks/useGmp';
import useTranslation from 'web/hooks/useTranslation';

/**
* Custom hook to handle the cloning of an entity.
*
* @param {string} name - The name of the entity to be cloned.
* @param {Object} [callbacks] - Optional callbacks for handling clone events.
* @param {Function} [callbacks.onCloneError] - Callback function to be called when cloning fails.
* @param {Function} [callbacks.onCloned] - Callback function to be called when cloning is successful.
* @param {Function} [callbacks.onInteraction] - Callback function to be called on interaction.
* @returns {Function} - A function that takes an entity and handles its cloning.
*/
const useEntityClone = (name, {onCloneError, onCloned, onInteraction} = {}) => {
const gmp = useGmp();
const cmd = gmp[name];
const [_] = useTranslation();

const handleInteraction = () => {
if (isDefined(onInteraction)) {
onInteraction();
}
};

const handleEntityClone = async entity => {
handleInteraction();

return actionFunction(
cmd.clone(entity),
onCloned,
onCloneError,
_('{{name}} cloned successfully.', {name: entity.name}),
);
};
return handleEntityClone;
};

export default useEntityClone;

0 comments on commit 434e357

Please sign in to comment.