From c125bc9965fe9829b9292e15c8426fbb513a7c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Mon, 17 Jun 2024 13:32:41 +0200 Subject: [PATCH] Add: Add a BulkTags component for handling tagging of entities The BulkTags component provides dialogs and functions for tagging several entities with an existing or new tag. --- src/web/entities/BulkTags.jsx | 236 ++++++++++++++++++++++++ src/web/entities/__tests__/BulkTags.jsx | 192 +++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 src/web/entities/BulkTags.jsx create mode 100644 src/web/entities/__tests__/BulkTags.jsx diff --git a/src/web/entities/BulkTags.jsx b/src/web/entities/BulkTags.jsx new file mode 100644 index 0000000000..42e406d7da --- /dev/null +++ b/src/web/entities/BulkTags.jsx @@ -0,0 +1,236 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React, {useCallback, useEffect, useState} from 'react'; + +import _ from 'gmp/locale'; +import {YES_VALUE} from 'gmp/parser'; +import {isDefined} from 'gmp/utils/identity'; +import {apiType, getEntityType, typeName} from 'gmp/utils/entitytype'; + +import PropTypes from 'web/utils/proptypes'; +import SelectionType from 'web/utils/selectiontype'; + +import useGmp from 'web/utils/useGmp'; +import useUserSessionTimeout from 'web/utils/useUserSessionTimeout'; + +import TagDialog from 'web/pages/tags/dialog'; + +import TagsDialog from './tagsdialog'; + +const getEntityIds = (entityArray = []) => entityArray.map(entity => entity.id); + +const getMultiTagEntitiesCount = ( + pageEntities, + counts, + selectedEntities, + selectionType, +) => { + if (selectionType === SelectionType.SELECTION_USER) { + // support set and array + return isDefined(selectedEntities?.size) + ? selectedEntities.size + : selectedEntities.length; + } + + if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) { + return pageEntities.length; + } + + return counts.filtered; +}; + +const BulkTags = ({ + entities, + selectedEntities, + filter, + selectionType, + entitiesCounts, + onClose, +}) => { + const gmp = useGmp(); + const [, renewSession] = useUserSessionTimeout(); + const [tag, setTag] = useState({}); + const [tagDialogVisible, setTagDialogVisible] = useState(false); + const [tags, setTags] = useState([]); + const [error, setError] = useState(); + + const entitiesType = getEntityType(entities[0]); + // if there are no entities, BulkTagComponent is not rendered. + + const getTagsByType = useCallback(() => { + const tagFilter = `resource_type=${apiType(entitiesType)}`; + + return gmp.tags + .getAll({filter: tagFilter}) + .then(resp => { + setTags(resp.data); + }) + .catch(setError); + }, [gmp.tags, entitiesType]); + + useEffect(() => { + getTagsByType(); + }, [getTagsByType]); + + const multiTagEntitiesCount = getMultiTagEntitiesCount( + entities, + entitiesCounts, + selectedEntities, + selectionType, + ); + + const closeTagDialog = useCallback(() => { + setTagDialogVisible(false); + }, []); + + const openTagDialog = useCallback(() => { + renewSession(); + setTagDialogVisible(true); + }, [renewSession]); + + const handleCreateTag = useCallback( + data => { + renewSession(); + + return gmp.tag + .create(data) + .then(response => gmp.tag.get(response.data)) + .then(response => { + const newTags = [...tags, response.data]; + setTags(newTags); + setTag(response.data); + }) + .then(closeTagDialog) + .catch(setError); + }, + [closeTagDialog, gmp.tag, renewSession, tags], + ); + + const handleCloseTagDialog = useCallback(() => { + closeTagDialog(); + renewSession(); + }, [closeTagDialog, renewSession]); + + const handleTagChange = useCallback( + id => { + renewSession(); + return gmp.tag + .get({id}) + .then(resp => { + setTag(resp.data); + }) + .catch(setError); + }, + [renewSession, gmp.tag], + ); + + const handleCloseTagsDialog = useCallback(() => { + onClose(); + }, [onClose]); + + const handleErrorClose = useCallback(() => { + setError(); + }, []); + + const handleAddMultiTag = useCallback( + ({comment, id, name, value = ''}) => { + let tagEntitiesIds; + let loadedFilter; + + if (selectionType === SelectionType.SELECTION_USER) { + tagEntitiesIds = getEntityIds(selectedEntities); + loadedFilter = null; + } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) { + tagEntitiesIds = getEntityIds(entities); + loadedFilter = null; + } else { + loadedFilter = filter.all().toFilterString(); + tagEntitiesIds = null; + } + + return gmp.tag + .save({ + active: YES_VALUE, + comment, + filter: loadedFilter, + id, + name, + resource_ids: tagEntitiesIds, + resource_type: entitiesType, + resources_action: 'add', + value, + }) + .then(onClose) + .catch(setError); + }, + [ + entities, + entitiesType, + filter, + gmp.tag, + onClose, + selectedEntities, + selectionType, + ], + ); + + const resourceTypes = [[entitiesType, typeName(entitiesType)]]; + + let title; + if (selectionType === SelectionType.SELECTION_USER) { + title = _('Add Tag to Selection'); + } else if (selectionType === SelectionType.SELECTION_PAGE_CONTENTS) { + title = _('Add Tag to Page Contents'); + } else { + title = _('Add Tag to All Filtered'); + } + + return ( + <> + + {tagDialogVisible && ( + + )} + + ); +}; + +BulkTags.propTypes = { + entities: PropTypes.arrayOf(PropTypes.model).isRequired, + entitiesCounts: PropTypes.counts.isRequired, + filter: PropTypes.filter.isRequired, + selectedEntities: PropTypes.arrayOf(PropTypes.model).isRequired, + selectionType: PropTypes.oneOf([ + SelectionType.SELECTION_PAGE_CONTENTS, + SelectionType.SELECTION_USER, + SelectionType.SELECTION_FILTER, + ]).isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default BulkTags; diff --git a/src/web/entities/__tests__/BulkTags.jsx b/src/web/entities/__tests__/BulkTags.jsx new file mode 100644 index 0000000000..4aec9f2ddd --- /dev/null +++ b/src/web/entities/__tests__/BulkTags.jsx @@ -0,0 +1,192 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; + +import { + screen, + rendererWith, + getByTestId, + fireEvent, + getAllByTitle, + wait, +} from 'web/utils/testing'; + +import {setSessionTimeout} from 'web/store/usersettings/actions'; + +import {isDefined} from 'gmp/utils/identity'; + +import date from 'gmp/models/date'; +import Filter from 'gmp/models/filter'; +import Tag from 'gmp/models/tag'; +import Task from 'gmp/models/task'; + +import SelectionType from 'web/utils/selectiontype'; + +import BulkTags from '../BulkTags'; + +const getDialog = () => { + return screen.getByRole('dialog'); +}; + +const getDialogTitle = dialog => { + dialog = isDefined(dialog) ? dialog : getDialog(); + return getByTestId(dialog, 'dialog-title-bar'); +}; + +const getDialogSaveButton = dialog => { + dialog = isDefined(dialog) ? dialog : getDialog(); + return getByTestId(dialog, 'dialog-save-button'); +}; + +describe('BulkTags', () => { + test('should render the BulkTags component', () => { + const entities = [Task.fromElement({id: 1}), Task.fromElement({id: 2})]; + const entitiesCounts = {filtered: 2, all: 2}; + const filter = Filter.fromString(''); + const selectedEntities = []; + const onClose = testing.fn(); + const getAllTags = testing + .fn() + .mockResolvedValue({data: [Tag.fromElement({id: 1})]}); + const gmp = {tags: {getAll: getAllTags}}; + const timeout = date('2019-10-10'); + + const {render, store} = rendererWith({gmp, store: true}); + + store.dispatch(setSessionTimeout(timeout)); + + render( + , + ); + const dialog = getDialog(); + expect(dialog).toBeInTheDocument(); + }); + + test('should allow to tag all filtered entities', () => { + const entities = [Task.fromElement({id: 1}), Task.fromElement({id: 2})]; + const entitiesCounts = {filtered: 2, all: 2}; + const filter = Filter.fromString(''); + const selectedEntities = []; + const onClose = testing.fn(); + const getAllTags = testing + .fn() + .mockResolvedValue({data: [Tag.fromElement({id: 1})]}); + const gmp = {tags: {getAll: getAllTags}}; + const timeout = date('2019-10-10'); + + const {render, store} = rendererWith({gmp, store: true}); + + store.dispatch(setSessionTimeout(timeout)); + + render( + , + ); + const title = getDialogTitle(); + expect(title).toHaveTextContent('Add Tag to All Filtered'); + }); + + test('should allow to tag tasks with a new tag', async () => { + const entities = [Task.fromElement({id: '1'}), Task.fromElement({id: '2'})]; + const entitiesCounts = {filtered: 2, all: 2}; + const filter = Filter.fromString(''); + const selectedEntities = []; + const onClose = testing.fn(); + const createTag = testing.fn().mockResolvedValue({data: {id: '2'}}); + const getTag = testing + .fn() + .mockResolvedValue({data: Tag.fromElement({id: '2'})}); + const getAllTags = testing + .fn() + .mockResolvedValue({data: [Tag.fromElement({id: '1'})]}); + const getAllResourceNames = testing.fn().mockResolvedValue({data: []}); + const saveTag = testing.fn().mockResolvedValue({data: {id: '2'}}); + const gmp = { + tags: {getAll: getAllTags}, + resourcenames: {getAll: getAllResourceNames}, + tag: { + create: createTag, + get: getTag, + save: saveTag, + }, + }; + const timeout = date('2019-10-10'); + + const {render, store} = rendererWith({gmp, store: true}); + + store.dispatch(setSessionTimeout(timeout)); + + render( + , + ); + + const dialog = getDialog(); + const newTag = getAllByTitle(dialog, 'Create a new Tag')[0]; + + fireEvent.click(newTag); + expect(getAllResourceNames).toHaveBeenCalledWith({ + resource_type: 'task', + }); + + const dialogs = screen.queryAllByRole('dialog'); + expect(dialogs.length).toEqual(2); + + const tagsDialog = dialog[0]; + const tagDialog = dialogs[1]; + const saveTagButton = getDialogSaveButton(tagDialog); + + fireEvent.click(saveTagButton); + + await wait(); + + expect(createTag).toHaveBeenCalledWith({ + active: 1, + comment: '', + name: 'default:unnamed', + resource_ids: [], + resource_type: 'task', + resources: [], + value: '', + }); + expect(getTag).toHaveBeenCalledWith({id: '2'}); + + const saveTagsButton = getDialogSaveButton(tagsDialog); + + fireEvent.click(saveTagsButton); + + expect(saveTag).toHaveBeenCalledWith({ + active: 1, + comment: '', + filter: null, + id: '2', + name: undefined, + resource_ids: ['1', '2'], + resource_type: 'task', + resources_action: 'add', + value: '', + }); + }); +});