Skip to content

Commit

Permalink
Add: Extract a useEntitySave 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 7fa7361 commit 75b9024
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 10 deletions.
19 changes: 9 additions & 10 deletions src/web/entity/EntityComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {isDefined} from 'gmp/utils/identity';
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';
Expand Down Expand Up @@ -39,18 +40,16 @@ const EntityComponent = ({
const handleEntityDownload = useEntityDownload(name, {
onDownloadError,
onDownloaded,
onInteraction: handleInteraction,
onInteraction,
});

const handleEntitySave = async data => {
handleInteraction();

if (isDefined(data.id)) {
return actionFunction(cmd.save(data), onSaved, onSaveError);
}

return actionFunction(cmd.create(data), onCreated, onCreateError);
};
const handleEntitySave = useEntitySave(name, {
onSaveError,
onSaved,
onCreated,
onCreateError,
onInteraction,
});

const handleEntityDelete = useEntityDelete(name, {
onDeleteError,
Expand Down
134 changes: 134 additions & 0 deletions src/web/entity/hooks/__tests__/useEntitySave.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* SPDX-FileCopyrightText: 2025 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

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

describe('useEntitySave', () => {
test('should allow to save an entity', async () => {
const entity = {id: '123'};
const saveEntity = testing.fn().mockResolvedValue(entity);
const gmp = {
foo: {save: saveEntity},
};
const onSaved = testing.fn();
const onSaveError = testing.fn();
const onCreated = testing.fn();
const onCreateError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntitySave('foo', {
onSaved,
onSaveError,
onCreated,
onCreateError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(saveEntity).toHaveBeenCalledWith(entity);
expect(onSaved).toHaveBeenCalledWith(entity);
expect(onSaveError).not.toHaveBeenCalled();
expect(onInteraction).toHaveBeenCalledOnce();
});

test('should call onSaveError when saving an entity fails', async () => {
const saveEntity = testing.fn().mockRejectedValue(new Error('error'));
const entity = {id: '123'};
const gmp = {
foo: {save: saveEntity},
};
const onSaved = testing.fn();
const onSaveError = testing.fn();
const onCreated = testing.fn();
const onCreateError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntitySave('foo', {
onSaved,
onSaveError,
onCreated,
onCreateError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(saveEntity).toHaveBeenCalledWith(entity);
expect(onSaved).not.toHaveBeenCalled();
expect(onSaveError).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledOnce();
});

test('should allow to create an entity', async () => {
const entity = {name: 'foo'};
const createEntity = testing.fn().mockResolvedValue(entity);
const gmp = {
foo: {create: createEntity},
};
const onSaved = testing.fn();
const onSaveError = testing.fn();
const onCreated = testing.fn();
const onCreateError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntitySave('foo', {
onSaved,
onSaveError,
onCreated,
onCreateError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(createEntity).toHaveBeenCalledWith(entity);
expect(onCreated).toHaveBeenCalledWith(entity);
expect(onCreateError).not.toHaveBeenCalled();
expect(onInteraction).toHaveBeenCalledOnce();
});

test('should call onCreateError when creating an entity fails', async () => {
const createEntity = testing.fn().mockRejectedValue(new Error('error'));
const entity = {name: 'foo'};
const gmp = {
foo: {create: createEntity},
};
const onSaved = testing.fn();
const onSaveError = testing.fn();
const onCreated = testing.fn();
const onCreateError = testing.fn();
const onInteraction = testing.fn();
const {renderHook} = rendererWith({gmp, store: true});

const {result} = renderHook(() =>
useEntitySave('foo', {
onSaved,
onSaveError,
onCreated,
onCreateError,
onInteraction,
}),
);
expect(result.current).toBeDefined;
result.current(entity);
await wait();
expect(createEntity).toHaveBeenCalledWith(entity);
expect(onCreated).not.toHaveBeenCalled();
expect(onCreateError).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledOnce();
});
});
48 changes: 48 additions & 0 deletions src/web/entity/hooks/useEntitySave.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* 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';

/**
* Custom hook to handle saving or creating an entity.
*
* @param {string} name - The name of the entity.
* @param {Object} [callbacks={}] - Optional callbacks for various save events.
* @param {Function} [callbacks.onSaveError] - Callback function to be called on save error.
* @param {Function} [callbacks.onSaved] - Callback function to be called when the entity is saved.
* @param {Function} [callbacks.onCreated] - Callback function to be called when the entity is created.
* @param {Function} [callbacks.onCreateError] - Callback function to be called on create error.
* @param {Function} [callbacks.onInteraction] - Callback function to be called on interaction.
* @returns {Function} - A function to handle saving the entity. The function takes an entity as an argument.
* If the entity has an id, it will be saved, otherwise it will be created.
*/
const useEntitySave = (
name,
{onSaveError, onSaved, onCreated, onCreateError, onInteraction} = {},
) => {
const gmp = useGmp();
const cmd = gmp[name];

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

const handleEntitySave = async data => {
handleInteraction();

if (isDefined(data.id)) {
return actionFunction(cmd.save(data), onSaved, onSaveError);
}

return actionFunction(cmd.create(data), onCreated, onCreateError);
};
return handleEntitySave;
};

export default useEntitySave;

0 comments on commit 75b9024

Please sign in to comment.