Skip to content

Commit

Permalink
feat: Taxonomy delete dialog (#684)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ChrisChV authored Dec 12, 2023
1 parent dcabb77 commit 1eff489
Show file tree
Hide file tree
Showing 28 changed files with 634 additions and 119 deletions.
6 changes: 3 additions & 3 deletions src/content-tags-drawer/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>}
* @returns {Promise<import("./types.mjs").TaxonomyTagsData>}
*/
export async function getTaxonomyTagsData(taxonomyId, fullPathProvided) {
const { data } = await getAuthenticatedHttpClient().get(
Expand All @@ -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<Object>}
* @returns {Promise<import("./types.mjs").ContentTaxonomyTagsData>}
*/
export async function getContentTaxonomyTagsData(contentId) {
const { data } = await getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId));
Expand All @@ -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<Object>}
* @returns {Promise<import("./types.mjs").ContentData>}
*/
export async function getContentData(contentId) {
const { data } = await getAuthenticatedHttpClient().get(getContentDataApiUrl(contentId));
Expand Down
3 changes: 0 additions & 3 deletions src/content-tags-drawer/data/apiHooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<import("./types.mjs").TaxonomyTagsData>}
*/
export const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => (
useQuery({
Expand All @@ -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<import("./types.mjs").ContentTaxonomyTagsData>}
*/
export const useContentTaxonomyTagsData = (contentId) => (
useQuery({
Expand All @@ -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<import("./types.mjs").ContentData>}
*/
export const useContentData = (contentId) => (
useQuery({
Expand Down
5 changes: 0 additions & 5 deletions src/content-tags-drawer/data/types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,3 @@
* @property {TaxonomyTagData[]} results
*/

/**
* @typedef {Object} UseQueryResult
* @property {Object} data
* @property {string} status
*/
2 changes: 1 addition & 1 deletion src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
35 changes: 28 additions & 7 deletions src/taxonomy/TaxonomyLayout.jsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<div className="bg-light-400">
<Header isHiddenMainMenu />
<Outlet />
<StudioFooter />
</div>
);
const TaxonomyLayout = () => {
// Use `setToastMessage` to show the toast.
const [toastMessage, setToastMessage] = useState(null);

const context = useMemo(() => ({
toastMessage, setToastMessage,
}), []);

return (
<TaxonomyContext.Provider value={context}>
<div className="bg-light-400">
<Header isHiddenMainMenu />
<Outlet />
<StudioFooter />
<Toast
show={toastMessage !== null}
onClose={() => setToastMessage(null)}
data-testid="taxonomy-toast"
>
{toastMessage}
</Toast>
</div>
</TaxonomyContext.Provider>
);
};

export default TaxonomyLayout;
21 changes: 19 additions & 2 deletions src/taxonomy/TaxonomyLayout.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => <div data-testid="mock-header" />));
jest.mock('@edx/frontend-component-footer', () => ({
StudioFooter: jest.fn(() => <div data-testid="mock-footer" />),
Expand All @@ -17,6 +17,15 @@ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Outlet: jest.fn(() => <div data-testid="mock-content" />),
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn((initial) => {
if (initial === null) {
return [toastMessage, jest.fn()];
}
return [initial, jest.fn()];
}),
}));

const RootWrapper = () => (
<AppProvider store={store}>
Expand Down Expand Up @@ -45,4 +54,12 @@ describe('<TaxonomyLayout />', async () => {
expect(getByTestId('mock-content')).toBeInTheDocument();
expect(getByTestId('mock-footer')).toBeInTheDocument();
});

it('should show toast', async () => {
const { getByTestId, getByText } = render(<RootWrapper />);
act(() => {
expect(getByTestId('taxonomy-toast')).toBeInTheDocument();
expect(getByText(toastMessage)).toBeInTheDocument();
});
});
});
26 changes: 21 additions & 5 deletions src/taxonomy/TaxonomyListPage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import {
CardView,
Container,
Expand All @@ -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 = () => (
Expand Down Expand Up @@ -70,11 +83,14 @@ const TaxonomyListPage = () => {
{
accessor: 'systemDefined',
},
{
accessor: 'tagsCount',
},
]}
>
<CardView
className="bg-light-400 p-5"
CardComponent={TaxonomyCard}
CardComponent={(row) => TaxonomyCard({ ...row, onDeleteTaxonomy })}
/>
</DataTable>
)}
Expand Down
63 changes: 49 additions & 14 deletions src/taxonomy/TaxonomyListPage.test.jsx
Original file line number Diff line number Diff line change
@@ -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
<button type="button" data-testid="test-delete-button" onClick={() => onClickMenuItem('delete')} />
)));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<TaxonomyListPage intl={injectIntl} />
</IntlProvider>
</AppProvider>
);
const RootWrapper = () => {
const context = useMemo(() => ({
toastMessage: null,
setToastMessage: mockSetToastMessage,
}), []);

return (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<TaxonomyContext.Provider value={context}>
<TaxonomyListPage intl={injectIntl} />
</TaxonomyContext.Provider>
</IntlProvider>
</AppProvider>
);
};

describe('<TaxonomyListPage />', async () => {
beforeEach(async () => {
Expand Down Expand Up @@ -54,15 +76,28 @@ describe('<TaxonomyListPage />', async () => {
it('shows the data table after the query is complete', async () => {
useIsTaxonomyListDataLoaded.mockReturnValue(true);
useTaxonomyListDataResponse.mockReturnValue({
results: [{
id: 1,
name: 'Taxonomy',
description: 'This is a description',
}],
results: taxonomies,
});
await act(async () => {
const { getByTestId } = render(<RootWrapper />);
expect(getByTestId('taxonomy-card-1')).toBeInTheDocument();
});
});

it('should show the success toast after delete', async () => {
useIsTaxonomyListDataLoaded.mockReturnValue(true);
useTaxonomyListDataResponse.mockReturnValue({
results: taxonomies,
});
mockDeleteTaxonomy.mockImplementationOnce(async (params, callbacks) => {
callbacks.onSuccess();
});
const { getByTestId, getByLabelText } = render(<RootWrapper />);
fireEvent.click(getByTestId('test-delete-button'));
fireEvent.change(getByLabelText('Type DELETE to confirm'), { target: { value: 'DELETE' } });
fireEvent.click(getByTestId('delete-button'));

expect(mockDeleteTaxonomy).toBeCalledTimes(1);
expect(mockSetToastMessage).toBeCalledWith(`"${taxonomies[0].name}" deleted`);
});
});
7 changes: 7 additions & 0 deletions src/taxonomy/common/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* eslint-disable import/prefer-default-export */
import React from 'react';

export const TaxonomyContext = React.createContext({
toastMessage: null,
setToastMessage: null,
});
12 changes: 11 additions & 1 deletion src/taxonomy/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,27 @@ export const getExportTaxonomyApiUrl = (pk, format) => new URL(
`api/content_tagging/v1/taxonomies/${pk}/export/?output_format=${format}&download=1`,
getApiBaseUrl(),
).href;
export const getTaxonomyApiUrl = (pk) => new URL(`api/content_tagging/v1/taxonomies/${pk}/`, getApiBaseUrl()).href;

/**
* Get list of taxonomies.
* @param {string} org Optioanl organization query param
* @returns {Promise<Object>}
* @returns {Promise<import("./types.mjs").TaxonomyListData>}
*/
export async function getTaxonomyListData(org) {
const { data } = await getAuthenticatedHttpClient().get(getTaxonomyListApiUrl(org));
return camelCaseObject(data);
}

/**
* Delete a Taxonomy
* @param {number} pk
* @returns {Promise<Object>}
*/
export async function deleteTaxonomy(pk) {
await getAuthenticatedHttpClient().delete(getTaxonomyApiUrl(pk));
}

/**
* Downloads the file of the exported taxonomy
* @param {number} pk
Expand Down
9 changes: 9 additions & 0 deletions src/taxonomy/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getExportTaxonomyApiUrl,
getTaxonomyListData,
getTaxonomyExportFile,
getTaxonomyApiUrl,
deleteTaxonomy,
} from './api';

let axiosMock;
Expand Down Expand Up @@ -59,6 +61,13 @@ describe('taxonomy api calls', () => {
expect(result).toEqual(taxonomyListMock);
});

it('should delete a taxonomy', async () => {
axiosMock.onDelete(getTaxonomyApiUrl()).reply(200);
await deleteTaxonomy();

expect(axiosMock.history.delete[0].url).toEqual(getTaxonomyApiUrl());
});

it('should set window.location.href correctly', () => {
const pk = 1;
const format = 'json';
Expand Down
Loading

0 comments on commit 1eff489

Please sign in to comment.