From 1eff48915873f8867af96909bc9e9aea3c5dec20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Tue, 12 Dec 2023 07:24:39 -0500 Subject: [PATCH] feat: Taxonomy delete dialog (#684) This adds: New submenu 'Delete' on the Taxonomy card menu Delete Dialog with the functionality to delete a Taxonomy Show a Toast after delete the Taxonomy Enable export for System defined Taxonomies --- src/content-tags-drawer/data/api.js | 6 +- src/content-tags-drawer/data/apiHooks.jsx | 3 - src/content-tags-drawer/data/types.mjs | 5 - src/index.scss | 2 +- src/taxonomy/TaxonomyLayout.jsx | 35 ++++-- src/taxonomy/TaxonomyLayout.test.jsx | 21 +++- src/taxonomy/TaxonomyListPage.jsx | 26 +++- src/taxonomy/TaxonomyListPage.test.jsx | 63 +++++++--- src/taxonomy/common/context.js | 7 ++ src/taxonomy/data/api.js | 12 +- src/taxonomy/data/api.test.js | 9 ++ src/taxonomy/data/apiHooks.jsx | 23 +++- src/taxonomy/data/apiHooks.test.jsx | 34 +++++- src/taxonomy/data/types.mjs | 11 +- src/taxonomy/delete-dialog/DeleteDialog.scss | 6 + src/taxonomy/delete-dialog/index.jsx | 99 +++++++++++++++ src/taxonomy/delete-dialog/messages.js | 30 +++++ src/taxonomy/export-modal/index.jsx | 4 +- src/taxonomy/index.scss | 2 + src/taxonomy/messages.js | 4 + .../taxonomy-card/TaxonomyCard.test.jsx | 56 ++++++++- .../taxonomy-card/TaxonomyCardMenu.jsx | 29 +++-- src/taxonomy/taxonomy-card/index.jsx | 46 +++++-- src/taxonomy/taxonomy-card/messages.js | 4 + .../taxonomy-detail/TaxonomyDetailMenu.jsx | 18 ++- .../taxonomy-detail/TaxonomyDetailPage.jsx | 80 ++++++++---- .../TaxonomyDetailPage.test.jsx | 114 ++++++++++++++++-- src/taxonomy/taxonomy-detail/messages.js | 4 + 28 files changed, 634 insertions(+), 119 deletions(-) create mode 100644 src/taxonomy/common/context.js create mode 100644 src/taxonomy/delete-dialog/DeleteDialog.scss create mode 100644 src/taxonomy/delete-dialog/index.jsx create mode 100644 src/taxonomy/delete-dialog/messages.js create mode 100644 src/taxonomy/index.scss diff --git a/src/content-tags-drawer/data/api.js b/src/content-tags-drawer/data/api.js index 562bfaa1ec..a6082b33da 100644 --- a/src/content-tags-drawer/data/api.js +++ b/src/content-tags-drawer/data/api.js @@ -12,7 +12,7 @@ export const getContentDataApiUrl = (contentId) => new URL(`/xblock/outline/${co * @param {number} taxonomyId The id of the taxonomy to fetch tags for * @param {string} fullPathProvided Optional param that contains the full URL to fetch data * If provided, we use it instead of generating the URL. This is usually for fetching subTags - * @returns {Promise} + * @returns {Promise} */ export async function getTaxonomyTagsData(taxonomyId, fullPathProvided) { const { data } = await getAuthenticatedHttpClient().get( @@ -24,7 +24,7 @@ export async function getTaxonomyTagsData(taxonomyId, fullPathProvided) { /** * Get the tags that are applied to the content object * @param {string} contentId The id of the content object to fetch the applied tags for - * @returns {Promise} + * @returns {Promise} */ export async function getContentTaxonomyTagsData(contentId) { const { data } = await getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId)); @@ -34,7 +34,7 @@ export async function getContentTaxonomyTagsData(contentId) { /** * Fetch meta data (eg: display_name) about the content object (unit/compoenent) * @param {string} contentId The id of the content object (unit/component) - * @returns {Promise} + * @returns {Promise} */ export async function getContentData(contentId) { const { data } = await getAuthenticatedHttpClient().get(getContentDataApiUrl(contentId)); diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index df5f49bebf..97ffde2fe2 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -12,7 +12,6 @@ import { * @param {number} taxonomyId The id of the taxonomy to fetch tags for * @param {string} fullPathProvided Optional param that contains the full URL to fetch data * If provided, we use it instead of generating the URL. This is usually for fetching subTags - * @returns {import("@tanstack/react-query").UseQueryResult} */ export const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => ( useQuery({ @@ -24,7 +23,6 @@ export const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => ( /** * Builds the query to get the taxonomy tags applied to the content object * @param {string} contentId The id of the content object to fetch the applied tags for - * @returns {import("@tanstack/react-query").UseQueryResult} */ export const useContentTaxonomyTagsData = (contentId) => ( useQuery({ @@ -36,7 +34,6 @@ export const useContentTaxonomyTagsData = (contentId) => ( /** * Builds the query to get meta data about the content object * @param {string} contentId The id of the content object (unit/component) - * @returns {import("@tanstack/react-query").UseQueryResult} */ export const useContentData = (contentId) => ( useQuery({ diff --git a/src/content-tags-drawer/data/types.mjs b/src/content-tags-drawer/data/types.mjs index 00b3fefd4c..c16cb45ea6 100644 --- a/src/content-tags-drawer/data/types.mjs +++ b/src/content-tags-drawer/data/types.mjs @@ -100,8 +100,3 @@ * @property {TaxonomyTagData[]} results */ -/** - * @typedef {Object} UseQueryResult - * @property {Object} data - * @property {string} status - */ diff --git a/src/index.scss b/src/index.scss index 6b7a1f42ae..2a3dc06794 100755 --- a/src/index.scss +++ b/src/index.scss @@ -18,7 +18,7 @@ @import "course-updates/CourseUpdates"; @import "export-page/CourseExportPage"; @import "import-page/CourseImportPage"; -@import "taxonomy/taxonomy-card/TaxonomyCard"; +@import "taxonomy"; @import "files-and-videos"; @import "content-tags-drawer/TagBubble"; @import "course-outline/CourseOutline"; diff --git a/src/taxonomy/TaxonomyLayout.jsx b/src/taxonomy/TaxonomyLayout.jsx index eb992b2b42..c49c55ee84 100644 --- a/src/taxonomy/TaxonomyLayout.jsx +++ b/src/taxonomy/TaxonomyLayout.jsx @@ -1,14 +1,35 @@ +import React, { useMemo, useState } from 'react'; import { StudioFooter } from '@edx/frontend-component-footer'; import { Outlet } from 'react-router-dom'; +import { Toast } from '@edx/paragon'; import Header from '../header'; +import { TaxonomyContext } from './common/context'; -const TaxonomyLayout = () => ( -
-
- - -
-); +const TaxonomyLayout = () => { + // Use `setToastMessage` to show the toast. + const [toastMessage, setToastMessage] = useState(null); + + const context = useMemo(() => ({ + toastMessage, setToastMessage, + }), []); + + return ( + +
+
+ + + setToastMessage(null)} + data-testid="taxonomy-toast" + > + {toastMessage} + +
+
+ ); +}; export default TaxonomyLayout; diff --git a/src/taxonomy/TaxonomyLayout.test.jsx b/src/taxonomy/TaxonomyLayout.test.jsx index 924e7465e9..d833809997 100644 --- a/src/taxonomy/TaxonomyLayout.test.jsx +++ b/src/taxonomy/TaxonomyLayout.test.jsx @@ -2,13 +2,13 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import initializeStore from '../store'; import TaxonomyLayout from './TaxonomyLayout'; let store; - +const toastMessage = 'Hello, this is a toast!'; jest.mock('../header', () => jest.fn(() =>
)); jest.mock('@edx/frontend-component-footer', () => ({ StudioFooter: jest.fn(() =>
), @@ -17,6 +17,15 @@ jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), Outlet: jest.fn(() =>
), })); +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useState: jest.fn((initial) => { + if (initial === null) { + return [toastMessage, jest.fn()]; + } + return [initial, jest.fn()]; + }), +})); const RootWrapper = () => ( @@ -45,4 +54,12 @@ describe('', async () => { expect(getByTestId('mock-content')).toBeInTheDocument(); expect(getByTestId('mock-footer')).toBeInTheDocument(); }); + + it('should show toast', async () => { + const { getByTestId, getByText } = render(); + act(() => { + expect(getByTestId('taxonomy-toast')).toBeInTheDocument(); + expect(getByText(toastMessage)).toBeInTheDocument(); + }); + }); }); diff --git a/src/taxonomy/TaxonomyListPage.jsx b/src/taxonomy/TaxonomyListPage.jsx index 20287b693f..9a1e69ffd9 100644 --- a/src/taxonomy/TaxonomyListPage.jsx +++ b/src/taxonomy/TaxonomyListPage.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { CardView, Container, @@ -7,21 +7,34 @@ import { } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Helmet } from 'react-helmet'; - import SubHeader from '../generic/sub-header/SubHeader'; import getPageHeadTitle from '../generic/utils'; import messages from './messages'; import TaxonomyCard from './taxonomy-card'; -import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './data/apiHooks'; +import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded, useDeleteTaxonomy } from './data/apiHooks'; +import { TaxonomyContext } from './common/context'; const TaxonomyListPage = () => { const intl = useIntl(); + const deleteTaxonomy = useDeleteTaxonomy(); + const { setToastMessage } = useContext(TaxonomyContext); + + const onDeleteTaxonomy = React.useCallback((id, name) => { + deleteTaxonomy({ pk: id }, { + onSuccess: async () => { + setToastMessage(intl.formatMessage(messages.taxonomyDeleteToast, { name })); + }, + onError: async () => { + // TODO: display the error to the user + }, + }); + }, [setToastMessage]); + const useTaxonomyListData = () => { const taxonomyListData = useTaxonomyListDataResponse(); const isLoaded = useIsTaxonomyListDataLoaded(); return { taxonomyListData, isLoaded }; }; - const { taxonomyListData, isLoaded } = useTaxonomyListData(); const getHeaderButtons = () => ( @@ -70,11 +83,14 @@ const TaxonomyListPage = () => { { accessor: 'systemDefined', }, + { + accessor: 'tagsCount', + }, ]} > TaxonomyCard({ ...row, onDeleteTaxonomy })} /> )} diff --git a/src/taxonomy/TaxonomyListPage.test.jsx b/src/taxonomy/TaxonomyListPage.test.jsx index 8e68347568..17c95034e2 100644 --- a/src/taxonomy/TaxonomyListPage.test.jsx +++ b/src/taxonomy/TaxonomyListPage.test.jsx @@ -1,28 +1,50 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; -import { act, render } from '@testing-library/react'; +import { act, render, fireEvent } from '@testing-library/react'; import initializeStore from '../store'; import TaxonomyListPage from './TaxonomyListPage'; import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from './data/apiHooks'; +import { TaxonomyContext } from './common/context'; let store; +const mockSetToastMessage = jest.fn(); +const mockDeleteTaxonomy = jest.fn(); +const taxonomies = [{ + id: 1, + name: 'Taxonomy', + description: 'This is a description', +}]; jest.mock('./data/apiHooks', () => ({ useTaxonomyListDataResponse: jest.fn(), useIsTaxonomyListDataLoaded: jest.fn(), + useDeleteTaxonomy: () => mockDeleteTaxonomy, })); +jest.mock('./taxonomy-card/TaxonomyCardMenu', () => jest.fn(({ onClickMenuItem }) => ( + // eslint-disable-next-line jsx-a11y/control-has-associated-label +