feat: bare bones taxonomy detail page [FC-0036] (#655)
* feat: System-defined tooltip added * feat: Taxonomy card menu added. Export menu item added * feat: Modal for export taxonomy * feat: Connect with export API * test: Tests for API and selectors * feat: Use windows.location.href to call the export endpoint * test: ExportModal.test added * style: Delete unnecesary code * docs: README updated with taxonomy feature * style: TaxonomyCard updated to a better code style * style: injectIntl replaced by useIntl on taxonomy pages and components * refactor: Move and rename taxonomy UI components to match 0002 ADR * refactor: Move api to data to match with 0002 ADR * test: Refactor ExportModal tests * chore: Fix validations * chore: Lint * refactor: Moving hooks to apiHooks * feat: add taxonomy detail page * fix: address nits in PR review * refactor: move data/selectors to data/apiHooks and fix tests to mock useQuery. * fix: address nits in PR review * fix: replace taxonomy menu ModalPopup with Dropdown menu Avoids clicking through to the card when using the menu button to hide a card's menu. * fix: change taxonomy URLs * /taxonomy-list is now /taxonomies, and there's a temporary redirect * /taxonomy-list/🆔 is now /taxonomy/🆔 --------- Co-authored-by: Christofer <christofer@opencraft.com> Co-authored-by: XnpioChV <xnpiochv@gmail.com> Co-authored-by: Christofer Chavez <christofer@example.com> Co-authored-by: Jillian Vogel <jill@opencraft.com> Co-authored-by: Braden MacDonald <braden@opencraft.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
@@ -22,7 +22,7 @@ import CourseAuthoringRoutes from './CourseAuthoringRoutes';
|
||||
import Head from './head/Head';
|
||||
import { StudioHome } from './studio-home';
|
||||
import CourseRerun from './course-rerun';
|
||||
import { TaxonomyListPage } from './taxonomy';
|
||||
import { TaxonomyLayout, TaxonomyDetailPage, TaxonomyListPage } from './taxonomy';
|
||||
import { ContentTagsDrawer } from './content-tags-drawer';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
@@ -55,10 +55,14 @@ const App = () => {
|
||||
<Route path="/course_rerun/:courseId" element={<CourseRerun />} />
|
||||
{process.env.ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<>
|
||||
<Route
|
||||
path="/taxonomy-list"
|
||||
element={<TaxonomyListPage />}
|
||||
/>
|
||||
{/* TODO: remove this redirect once Studio's link is updated */}
|
||||
<Route path="/taxonomy-list" element={<Navigate to="/taxonomies" />} />
|
||||
<Route path="/taxonomies" element={<TaxonomyLayout />}>
|
||||
<Route index element={<TaxonomyListPage />} />
|
||||
</Route>
|
||||
<Route path="/taxonomy" element={<TaxonomyLayout />}>
|
||||
<Route path="/taxonomy/:taxonomyId" element={<TaxonomyDetailPage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/tagging/components/widget/:contentId"
|
||||
element={<ContentTagsDrawer />}
|
||||
|
||||
14
src/taxonomy/TaxonomyLayout.jsx
Normal file
14
src/taxonomy/TaxonomyLayout.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import Header from '../header';
|
||||
|
||||
const TaxonomyLayout = () => (
|
||||
<div className="bg-light-400">
|
||||
<Header isHiddenMainMenu />
|
||||
<Outlet />
|
||||
<StudioFooter />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TaxonomyLayout;
|
||||
48
src/taxonomy/TaxonomyLayout.test.jsx
Normal file
48
src/taxonomy/TaxonomyLayout.test.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
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 initializeStore from '../store';
|
||||
import TaxonomyLayout from './TaxonomyLayout';
|
||||
|
||||
let store;
|
||||
|
||||
jest.mock('../header', () => jest.fn(() => <div data-testid="mock-header" />));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
StudioFooter: jest.fn(() => <div data-testid="mock-footer" />),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
Outlet: jest.fn(() => <div data-testid="mock-content" />),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TaxonomyLayout />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<TaxonomyLayout />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('should render page correctly', async () => {
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
expect(getByTestId('mock-header')).toBeInTheDocument();
|
||||
expect(getByTestId('mock-content')).toBeInTheDocument();
|
||||
expect(getByTestId('mock-footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,9 +5,7 @@ import {
|
||||
DataTable,
|
||||
Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import Header from '../header';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import messages from './messages';
|
||||
import TaxonomyCard from './taxonomy-card';
|
||||
@@ -37,14 +35,6 @@ const TaxonomyListPage = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
body {
|
||||
background-color: #E9E6E4; /* light-400 */
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<Header isHiddenMainMenu />
|
||||
<div className="pt-4.5 pr-4.5 pl-4.5 pb-2 bg-light-100 box-shadow-down-2">
|
||||
<Container size="xl">
|
||||
<SubHeader
|
||||
@@ -93,7 +83,6 @@ const TaxonomyListPage = () => {
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
<StudioFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,7 +67,11 @@ const ExportModal = ({
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.taxonomyModalsCancelLabel)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary" onClick={onClickExport}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onClickExport}
|
||||
data-testid={`export-button-${taxonomyId}`}
|
||||
>
|
||||
{intl.formatMessage(messages.exportModalSubmitButtonLabel)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as TaxonomyListPage } from './TaxonomyListPage';
|
||||
export { default as TaxonomyLayout } from './TaxonomyLayout';
|
||||
export { TaxonomyDetailPage } from './taxonomy-detail';
|
||||
|
||||
56
src/taxonomy/tag-list/TagListTable.jsx
Normal file
56
src/taxonomy/tag-list/TagListTable.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// ts-check
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
DataTable,
|
||||
} from '@edx/paragon';
|
||||
import _ from 'lodash';
|
||||
import Proptypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
import messages from './messages';
|
||||
import { useTagListDataResponse, useTagListDataStatus } from './data/apiHooks';
|
||||
|
||||
const TagListTable = ({ taxonomyId }) => {
|
||||
const intl = useIntl();
|
||||
const [options, setOptions] = useState({
|
||||
pageIndex: 0,
|
||||
});
|
||||
const { isLoading } = useTagListDataStatus(taxonomyId, options);
|
||||
const tagList = useTagListDataResponse(taxonomyId, options);
|
||||
|
||||
const fetchData = (args) => {
|
||||
if (!_.isEqual(args, options)) {
|
||||
setOptions({ ...args });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
isPaginated
|
||||
manualPagination
|
||||
fetchData={fetchData}
|
||||
data={tagList?.results || []}
|
||||
itemCount={tagList?.count || 0}
|
||||
pageCount={tagList?.numPages || 0}
|
||||
initialState={options}
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tagListColumnValueHeader),
|
||||
accessor: 'value',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={intl.formatMessage(messages.noResultsFoundMessage)} />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
TagListTable.propTypes = {
|
||||
taxonomyId: Proptypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default TagListTable;
|
||||
67
src/taxonomy/tag-list/TagListTable.test.jsx
Normal file
67
src/taxonomy/tag-list/TagListTable.test.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
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 { useTagListData } from './data/api';
|
||||
import initializeStore from '../../store';
|
||||
import TagListTable from './TagListTable';
|
||||
|
||||
let store;
|
||||
|
||||
jest.mock('./data/api', () => ({
|
||||
useTagListData: jest.fn(),
|
||||
}));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TagListTable taxonomyId="1" />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<TagListPage />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useTagListData.mockReturnValue({
|
||||
isLoading: true,
|
||||
isFetched: false,
|
||||
});
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('loading');
|
||||
});
|
||||
|
||||
it('should render page correctly', async () => {
|
||||
useTagListData.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
isError: false,
|
||||
data: {
|
||||
count: 3,
|
||||
numPages: 1,
|
||||
results: [
|
||||
{ value: 'Tag 1' },
|
||||
{ value: 'Tag 2' },
|
||||
{ value: 'Tag 3' },
|
||||
],
|
||||
},
|
||||
});
|
||||
const { getAllByRole } = render(<RootWrapper />);
|
||||
const rows = getAllByRole('row');
|
||||
expect(rows.length).toBe(3 + 1); // 3 items plus header
|
||||
});
|
||||
});
|
||||
27
src/taxonomy/tag-list/data/api.js
Normal file
27
src/taxonomy/tag-list/data/api.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
const getTagListApiUrl = (taxonomyId, page) => new URL(
|
||||
`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/?page=${page + 1}`,
|
||||
getApiBaseUrl(),
|
||||
).href;
|
||||
|
||||
// ToDo: fix types
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @param {import('./types.mjs').QueryOptions} options
|
||||
* @returns {import('@tanstack/react-query').UseQueryResult<import('./types.mjs').TagListData>}
|
||||
*/ // eslint-disable-next-line import/prefer-default-export
|
||||
export const useTagListData = (taxonomyId, options) => {
|
||||
const { pageIndex } = options;
|
||||
return useQuery({
|
||||
queryKey: ['tagList', taxonomyId, pageIndex],
|
||||
queryFn: async () => {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getTagListApiUrl(taxonomyId, pageIndex));
|
||||
return camelCaseObject(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
27
src/taxonomy/tag-list/data/api.test.js
Normal file
27
src/taxonomy/tag-list/data/api.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useTagListData,
|
||||
} from './api';
|
||||
|
||||
const mockHttpClient = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(() => mockHttpClient),
|
||||
}));
|
||||
|
||||
describe('useTagListData', () => {
|
||||
it('should call useQuery with the correct parameters', () => {
|
||||
useTagListData('1', { pageIndex: 3 });
|
||||
|
||||
expect(useQuery).toHaveBeenCalledWith({
|
||||
queryKey: ['tagList', '1', 3],
|
||||
queryFn: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
41
src/taxonomy/tag-list/data/apiHooks.jsx
Normal file
41
src/taxonomy/tag-list/data/apiHooks.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// @ts-check
|
||||
import {
|
||||
useTagListData,
|
||||
} from './api';
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @param {import("./types.mjs").QueryOptions} options
|
||||
* @returns {Pick<import('@tanstack/react-query').UseQueryResult, "error" | "isError" | "isFetched" | "isLoading" | "isSuccess" >}
|
||||
*/ /* eslint-enable max-len */
|
||||
export const useTagListDataStatus = (taxonomyId, options) => {
|
||||
const {
|
||||
error,
|
||||
isError,
|
||||
isFetched,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
} = useTagListData(taxonomyId, options);
|
||||
return {
|
||||
error,
|
||||
isError,
|
||||
isFetched,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @param {import("./types.mjs").QueryOptions} options
|
||||
* @returns {import("./types.mjs").TagListData | undefined}
|
||||
*/
|
||||
export const useTagListDataResponse = (taxonomyId, options) => {
|
||||
const { isSuccess, data } = useTagListData(taxonomyId, options);
|
||||
if (isSuccess) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
45
src/taxonomy/tag-list/data/apiHooks.test.jsx
Normal file
45
src/taxonomy/tag-list/data/apiHooks.test.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useTagListDataStatus,
|
||||
useTagListDataResponse,
|
||||
} from './apiHooks';
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useTagListDataStatus', () => {
|
||||
it('should return status values', () => {
|
||||
const status = {
|
||||
error: undefined,
|
||||
isError: false,
|
||||
isFetched: true,
|
||||
isLoading: true,
|
||||
isSuccess: true,
|
||||
};
|
||||
|
||||
useQuery.mockReturnValueOnce(status);
|
||||
|
||||
const result = useTagListDataStatus(0, {});
|
||||
|
||||
expect(result).toEqual(status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useTagListDataResponse', () => {
|
||||
it('should return data when status is success', () => {
|
||||
useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' });
|
||||
|
||||
const result = useTagListDataResponse(0, {});
|
||||
|
||||
expect(result).toEqual('data');
|
||||
});
|
||||
|
||||
it('should return undefined when status is not success', () => {
|
||||
useQuery.mockReturnValueOnce({ isSuccess: false });
|
||||
|
||||
const result = useTagListDataResponse(0, {});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
28
src/taxonomy/tag-list/data/types.mjs
Normal file
28
src/taxonomy/tag-list/data/types.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {Object} QueryOptions
|
||||
* @property {number} pageIndex
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TagListData
|
||||
* @property {number} childCount
|
||||
* @property {number} depth
|
||||
* @property {string} externalId
|
||||
* @property {number} id
|
||||
* @property {string | null} parentValue
|
||||
* @property {string | null} subTagsUrl
|
||||
* @property {string} value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} TagData
|
||||
* @property {number} count
|
||||
* @property {number} currentPage
|
||||
* @property {string} next
|
||||
* @property {number} numPages
|
||||
* @property {string} previous
|
||||
* @property {TagListData[]} results
|
||||
* @property {number} start
|
||||
*/
|
||||
2
src/taxonomy/tag-list/index.js
Normal file
2
src/taxonomy/tag-list/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as TagListTable } from './TagListTable';
|
||||
14
src/taxonomy/tag-list/messages.js
Normal file
14
src/taxonomy/tag-list/messages.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
noResultsFoundMessage: {
|
||||
id: 'course-authoring.tag-list.no-results-found.message',
|
||||
defaultMessage: 'No results found',
|
||||
},
|
||||
tagListColumnValueHeader: {
|
||||
id: 'course-authoring.tag-list.column.value.header',
|
||||
defaultMessage: 'Value',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -89,22 +89,27 @@ describe('<TaxonomyCard />', async () => {
|
||||
});
|
||||
|
||||
test('should open and close menu on button click', () => {
|
||||
const { getByTestId, getByText } = render(<TaxonomyCardComponent original={data} />);
|
||||
const { getByTestId } = render(<TaxonomyCardComponent original={data} />);
|
||||
|
||||
// Menu closed
|
||||
// Menu closed/doesn't exist yet
|
||||
expect(() => getByTestId('taxonomy-card-menu-1')).toThrow();
|
||||
|
||||
// Click on the menu button to open
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-button-1'));
|
||||
|
||||
// Menu opened
|
||||
expect(getByTestId('taxonomy-card-menu-1')).toBeInTheDocument();
|
||||
expect(getByTestId('taxonomy-card-menu-1')).toBeVisible();
|
||||
|
||||
// Click on any element to close the menu
|
||||
fireEvent.click(getByText('Export'));
|
||||
// Click on button again to close the menu
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-button-1'));
|
||||
|
||||
// Menu closed
|
||||
expect(() => getByTestId('taxonomy-card-menu-1')).toThrow();
|
||||
// Jest bug: toBeVisible() isn't checking opacity correctly
|
||||
// expect(getByTestId('taxonomy-card-menu-1')).not.toBeVisible();
|
||||
expect(getByTestId('taxonomy-card-menu-1').style.opacity).toEqual('0');
|
||||
|
||||
// Menu button still visible
|
||||
expect(getByTestId('taxonomy-card-menu-button-1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open export modal on export menu click', () => {
|
||||
@@ -115,7 +120,7 @@ describe('<TaxonomyCard />', async () => {
|
||||
|
||||
// Click on export menu
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-button-1'));
|
||||
fireEvent.click(getByText('Export'));
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-export-1'));
|
||||
|
||||
// Modal opened
|
||||
expect(getByText('Select format to export')).toBeInTheDocument();
|
||||
@@ -132,11 +137,11 @@ describe('<TaxonomyCard />', async () => {
|
||||
|
||||
// Click on export menu
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-button-1'));
|
||||
fireEvent.click(getByText('Export'));
|
||||
fireEvent.click(getByTestId('taxonomy-card-menu-export-1'));
|
||||
|
||||
// Select JSON format and click on export
|
||||
fireEvent.click(getByText('JSON file'));
|
||||
fireEvent.click(getByText('Export'));
|
||||
fireEvent.click(getByTestId('export-button-1'));
|
||||
|
||||
// Modal closed
|
||||
expect(() => getByText('Select format to export')).toThrow();
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Dropdown,
|
||||
IconButton,
|
||||
ModalPopup,
|
||||
Menu,
|
||||
Icon,
|
||||
MenuItem,
|
||||
} from '@edx/paragon';
|
||||
import { MoreVert } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -15,38 +13,34 @@ const TaxonomyCardMenu = ({
|
||||
id, name, onClickMenuItem,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [menuIsOpen, setMenuIsOpen] = useState(false);
|
||||
const [menuTarget, setMenuTarget] = useState(null);
|
||||
|
||||
const onClickItem = (menuName) => {
|
||||
setMenuIsOpen(false);
|
||||
const onClickItem = (e, menuName) => {
|
||||
e.preventDefault();
|
||||
onClickMenuItem(menuName);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
variant="primary"
|
||||
onClick={() => setMenuIsOpen(true)}
|
||||
ref={setMenuTarget}
|
||||
<Dropdown onToggle={(isOpen, ev) => ev.preventDefault()}>
|
||||
<Dropdown.Toggle
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt={intl.formatMessage(messages.taxonomyMenuAlt, { name })}
|
||||
id={`taxonomy-card-menu-button-${id}`}
|
||||
data-testid={`taxonomy-card-menu-button-${id}`}
|
||||
/>
|
||||
<ModalPopup
|
||||
positionRef={menuTarget}
|
||||
isOpen={menuIsOpen}
|
||||
onClose={() => setMenuIsOpen(false)}
|
||||
>
|
||||
<Menu data-testid={`taxonomy-card-menu-${id}`}>
|
||||
{/* Add more menu items here */}
|
||||
<MenuItem className="taxonomy-menu-item" onClick={() => onClickItem('export')}>
|
||||
{intl.formatMessage(messages.taxonomyCardExportMenu)}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</ModalPopup>
|
||||
</>
|
||||
<Dropdown.Menu data-testid={`taxonomy-card-menu-${id}`}>
|
||||
{/* Add more menu items here */}
|
||||
<Dropdown.Item
|
||||
className="taxonomy-menu-item"
|
||||
data-testid={`taxonomy-card-menu-export-${id}`}
|
||||
onClick={(e) => onClickItem(e, 'export')}
|
||||
>
|
||||
{intl.formatMessage(messages.taxonomyCardExportMenu)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Popover,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
@@ -109,7 +110,13 @@ const TaxonomyCard = ({ className, original }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className={classNames('taxonomy-card', className)} data-testid={`taxonomy-card-${id}`}>
|
||||
<Card
|
||||
isClickable
|
||||
as={Link}
|
||||
to={`/taxonomy/${id}`}
|
||||
className={classNames('taxonomy-card', className)}
|
||||
data-testid={`taxonomy-card-${id}`}
|
||||
>
|
||||
<Card.Header
|
||||
title={name}
|
||||
subtitle={(
|
||||
|
||||
41
src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx
Normal file
41
src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// ts-check
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const TaxonomyDetailMenu = ({
|
||||
id, name, disabled, onClickMenuItem,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
id={id}
|
||||
title={intl.formatMessage(messages.actionsButtonLabel)}
|
||||
alt={intl.formatMessage(messages.actionsButtonAlt, { name })}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Dropdown.Item onClick={() => onClickMenuItem('export')}>
|
||||
{intl.formatMessage(messages.exportMenu)}
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
};
|
||||
|
||||
TaxonomyDetailMenu.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onClickMenuItem: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
TaxonomyDetailMenu.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default TaxonomyDetailMenu;
|
||||
116
src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
Normal file
116
src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx
Normal file
@@ -0,0 +1,116 @@
|
||||
// ts-check
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Container,
|
||||
Layout,
|
||||
} from '@edx/paragon';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert';
|
||||
import Loading from '../../generic/Loading';
|
||||
import SubHeader from '../../generic/sub-header/SubHeader';
|
||||
import taxonomyMessages from '../messages';
|
||||
import TaxonomyDetailMenu from './TaxonomyDetailMenu';
|
||||
import TaxonomyDetailSideCard from './TaxonomyDetailSideCard';
|
||||
import { TagListTable } from '../tag-list';
|
||||
import ExportModal from '../export-modal';
|
||||
import { useTaxonomyDetailDataResponse, useTaxonomyDetailDataStatus } from './data/apiHooks';
|
||||
|
||||
const TaxonomyDetailPage = () => {
|
||||
const intl = useIntl();
|
||||
const { taxonomyId } = useParams();
|
||||
const { isError, isFetched } = useTaxonomyDetailDataStatus(taxonomyId);
|
||||
const taxonomy = useTaxonomyDetailDataResponse(taxonomyId);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
|
||||
if (!isFetched) {
|
||||
return (
|
||||
<Loading />
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !taxonomy) {
|
||||
return (
|
||||
<ConnectionErrorAlert />
|
||||
);
|
||||
}
|
||||
|
||||
const renderModals = () => isExportModalOpen && (
|
||||
<ExportModal
|
||||
isOpen={isExportModalOpen}
|
||||
onClose={() => setIsExportModalOpen(false)}
|
||||
taxonomyId={taxonomy.id}
|
||||
taxonomyName={taxonomy.name}
|
||||
/>
|
||||
);
|
||||
|
||||
const onClickMenuItem = (menuName) => {
|
||||
switch (menuName) {
|
||||
case 'export':
|
||||
setIsExportModalOpen(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getHeaderActions = () => (
|
||||
<TaxonomyDetailMenu
|
||||
id={taxonomy.id}
|
||||
name={taxonomy.name}
|
||||
disabled={
|
||||
// We don't show the export menu, because the system-taxonomies
|
||||
// can't be exported. The API returns and error.
|
||||
// The entire menu has been disabled because currently only
|
||||
// the export menu exists.
|
||||
// ToDo: When adding more menus, change this logic to hide only the export menu.
|
||||
taxonomy.systemDefined
|
||||
}
|
||||
onClickMenuItem={onClickMenuItem}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pt-4.5 pr-4.5 pl-4.5 pb-2 bg-light-100 box-shadow-down-2">
|
||||
<Container size="xl">
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{ label: intl.formatMessage(taxonomyMessages.headerTitle), to: '/taxonomies/' },
|
||||
]}
|
||||
activeLabel={taxonomy.name}
|
||||
linkAs={Link}
|
||||
/>
|
||||
<SubHeader
|
||||
title={taxonomy.name}
|
||||
hideBorder
|
||||
headerActions={getHeaderActions()}
|
||||
/>
|
||||
</Container>
|
||||
</div>
|
||||
<div className="bg-light-400 m-4">
|
||||
<Container size="xl">
|
||||
<Layout
|
||||
lg={[{ span: 9 }, { span: 3 }]}
|
||||
md={[{ span: 9 }, { span: 3 }]}
|
||||
sm={[{ span: 9 }, { span: 3 }]}
|
||||
xs={[{ span: 9 }, { span: 3 }]}
|
||||
xl={[{ span: 9 }, { span: 3 }]}
|
||||
>
|
||||
<Layout.Element>
|
||||
<TagListTable taxonomyId={taxonomyId} />
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<TaxonomyDetailSideCard taxonomy={taxonomy} />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</Container>
|
||||
</div>
|
||||
{renderModals()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaxonomyDetailPage;
|
||||
111
src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx
Normal file
111
src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { useTaxonomyDetailData } from './data/api';
|
||||
import initializeStore from '../../store';
|
||||
import TaxonomyDetailPage from './TaxonomyDetailPage';
|
||||
|
||||
let store;
|
||||
|
||||
jest.mock('./data/api', () => ({
|
||||
useTaxonomyDetailData: jest.fn(),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
|
||||
useParams: () => ({
|
||||
taxonomyId: '1',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('./TaxonomyDetailSideCard', () => jest.fn(() => <>Mock TaxonomyDetailSideCard</>));
|
||||
jest.mock('../tag-list/TagListTable', () => jest.fn(() => <>Mock TagListTable</>));
|
||||
|
||||
const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TaxonomyDetailPage />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
describe('<TaxonomyDetailPage />', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isFetched: false,
|
||||
});
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('shows the connector error component if got some error', async () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isFetched: true,
|
||||
isError: true,
|
||||
});
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
expect(getByTestId('connectionErrorAlert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page and page title correctly', async () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
isError: false,
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'Test taxonomy',
|
||||
description: 'This is a description',
|
||||
systemDefined: false,
|
||||
},
|
||||
});
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
expect(getByRole('heading')).toHaveTextContent('Test taxonomy');
|
||||
});
|
||||
|
||||
it('should open export modal on export menu click', () => {
|
||||
useTaxonomyDetailData.mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
isError: false,
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'Test taxonomy',
|
||||
description: 'This is a description',
|
||||
},
|
||||
});
|
||||
|
||||
const { getByRole, getByText } = render(<RootWrapper />);
|
||||
|
||||
// Modal closed
|
||||
expect(() => getByText('Select format to export')).toThrow();
|
||||
|
||||
// Click on export menu
|
||||
fireEvent.click(getByRole('button'));
|
||||
fireEvent.click(getByText('Export'));
|
||||
|
||||
// Modal opened
|
||||
expect(getByText('Select format to export')).toBeInTheDocument();
|
||||
|
||||
// Click on cancel button
|
||||
fireEvent.click(getByText('Cancel'));
|
||||
|
||||
// Modal closed
|
||||
expect(() => getByText('Select format to export')).toThrow();
|
||||
});
|
||||
});
|
||||
32
src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx
Normal file
32
src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Card,
|
||||
} from '@edx/paragon';
|
||||
import Proptypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const TaxonomyDetailSideCard = ({ taxonomy }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header title={intl.formatMessage(messages.taxonomyDetailsHeader)} />
|
||||
<Card.Section title={intl.formatMessage(messages.taxonomyDetailsName)}>
|
||||
{taxonomy.name}
|
||||
</Card.Section>
|
||||
<Card.Divider className="ml-3 mr-3" />
|
||||
<Card.Section title={intl.formatMessage(messages.taxonomyDetailsDescription)}>
|
||||
{taxonomy.description}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
TaxonomyDetailSideCard.propTypes = {
|
||||
taxonomy: Proptypes.shape({
|
||||
name: Proptypes.string.isRequired,
|
||||
description: Proptypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default TaxonomyDetailSideCard;
|
||||
53
src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.test.jsx
Normal file
53
src/taxonomy/taxonomy-detail/TaxonomyDetailSideCard.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
|
||||
import TaxonomyDetailSideCard from './TaxonomyDetailSideCard';
|
||||
|
||||
let store;
|
||||
|
||||
const data = {
|
||||
id: 1,
|
||||
name: 'Taxonomy 1',
|
||||
description: 'This is a description',
|
||||
};
|
||||
|
||||
const TaxonomyCardComponent = ({ taxonomy }) => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en" messages={{}}>
|
||||
<TaxonomyDetailSideCard taxonomy={taxonomy} />
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
TaxonomyCardComponent.propTypes = {
|
||||
taxonomy: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
describe('<TaxonomyDetailSideCard/>', async () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
});
|
||||
|
||||
it('should render title and description of the card', () => {
|
||||
const { getByText } = render(<TaxonomyCardComponent taxonomy={data} />);
|
||||
expect(getByText(data.name)).toBeInTheDocument();
|
||||
expect(getByText(data.description)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
23
src/taxonomy/taxonomy-detail/data/api.js
Normal file
23
src/taxonomy/taxonomy-detail/data/api.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-check
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
const getTaxonomyDetailApiUrl = (taxonomyId) => new URL(
|
||||
`api/content_tagging/v1/taxonomies/${taxonomyId}/`,
|
||||
getApiBaseUrl(),
|
||||
).href;
|
||||
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @returns {import('@tanstack/react-query').UseQueryResult<import('./types.mjs').TaxonomyData>}
|
||||
*/ // eslint-disable-next-line import/prefer-default-export
|
||||
export const useTaxonomyDetailData = (taxonomyId) => (
|
||||
useQuery({
|
||||
queryKey: ['taxonomyDetail', taxonomyId],
|
||||
queryFn: () => getAuthenticatedHttpClient().get(getTaxonomyDetailApiUrl(taxonomyId))
|
||||
.then((response) => response.data)
|
||||
.then(camelCaseObject),
|
||||
})
|
||||
);
|
||||
27
src/taxonomy/taxonomy-detail/data/api.test.js
Normal file
27
src/taxonomy/taxonomy-detail/data/api.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useTaxonomyDetailData,
|
||||
} from './api';
|
||||
|
||||
const mockHttpClient = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(() => mockHttpClient),
|
||||
}));
|
||||
|
||||
describe('useTaxonomyDetailData', () => {
|
||||
it('should call useQuery with the correct parameters', () => {
|
||||
useTaxonomyDetailData('1');
|
||||
|
||||
expect(useQuery).toHaveBeenCalledWith({
|
||||
queryKey: ['taxonomyDetail', '1'],
|
||||
queryFn: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
36
src/taxonomy/taxonomy-detail/data/apiHooks.jsx
Normal file
36
src/taxonomy/taxonomy-detail/data/apiHooks.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// @ts-check
|
||||
import {
|
||||
useTaxonomyDetailData,
|
||||
} from './api';
|
||||
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @returns {Pick<import('@tanstack/react-query').UseQueryResult, "error" | "isError" | "isFetched" | "isSuccess">}
|
||||
*/
|
||||
export const useTaxonomyDetailDataStatus = (taxonomyId) => {
|
||||
const {
|
||||
isError,
|
||||
error,
|
||||
isFetched,
|
||||
isSuccess,
|
||||
} = useTaxonomyDetailData(taxonomyId);
|
||||
return {
|
||||
isError,
|
||||
error,
|
||||
isFetched,
|
||||
isSuccess,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} taxonomyId
|
||||
* @returns {import("./types.mjs").TaxonomyData | undefined}
|
||||
*/
|
||||
export const useTaxonomyDetailDataResponse = (taxonomyId) => {
|
||||
const { isSuccess, data } = useTaxonomyDetailData(taxonomyId);
|
||||
if (isSuccess) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
44
src/taxonomy/taxonomy-detail/data/apiHooks.test.jsx
Normal file
44
src/taxonomy/taxonomy-detail/data/apiHooks.test.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
useTaxonomyDetailDataStatus,
|
||||
useTaxonomyDetailDataResponse,
|
||||
} from './apiHooks';
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useTaxonomyDetailDataStatus', () => {
|
||||
it('should return status values', () => {
|
||||
const status = {
|
||||
isError: false,
|
||||
error: undefined,
|
||||
isFetched: true,
|
||||
isSuccess: true,
|
||||
};
|
||||
|
||||
useQuery.mockReturnValueOnce(status);
|
||||
|
||||
const result = useTaxonomyDetailDataStatus(0);
|
||||
|
||||
expect(result).toEqual(status);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useTaxonomyDetailDataResponse', () => {
|
||||
it('should return data when status is success', () => {
|
||||
useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' });
|
||||
|
||||
const result = useTaxonomyDetailDataResponse();
|
||||
|
||||
expect(result).toEqual('data');
|
||||
});
|
||||
|
||||
it('should return undefined when status is not success', () => {
|
||||
useQuery.mockReturnValueOnce({ isSuccess: false });
|
||||
|
||||
const result = useTaxonomyDetailDataResponse();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
19
src/taxonomy/taxonomy-detail/data/types.mjs
Normal file
19
src/taxonomy/taxonomy-detail/data/types.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @typedef {Object} TaxonomyData
|
||||
* @property {number} id
|
||||
* @property {string} name
|
||||
* @property {boolean} enabled
|
||||
* @property {boolean} allowMultiple
|
||||
* @property {boolean} allowFreeText
|
||||
* @property {boolean} systemDefined
|
||||
* @property {boolean} visibleToAuthors
|
||||
* @property {string[]} orgs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} UseQueryResult
|
||||
* @property {Object} data
|
||||
* @property {string} status
|
||||
*/
|
||||
2
src/taxonomy/taxonomy-detail/index.js
Normal file
2
src/taxonomy/taxonomy-detail/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// ts-check
|
||||
export { default as TaxonomyDetailPage } from './TaxonomyDetailPage'; // eslint-disable-line import/prefer-default-export
|
||||
31
src/taxonomy/taxonomy-detail/messages.js
Normal file
31
src/taxonomy/taxonomy-detail/messages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// ts-check
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
taxonomyDetailsHeader: {
|
||||
id: 'course-authoring.taxonomy-detail.side-card.header',
|
||||
defaultMessage: 'Taxonomy details',
|
||||
},
|
||||
taxonomyDetailsName: {
|
||||
id: 'course-authoring.taxonomy-detail.side-card.name',
|
||||
defaultMessage: 'Title',
|
||||
},
|
||||
taxonomyDetailsDescription: {
|
||||
id: 'course-authoring.taxonomy-detail.side-card.description',
|
||||
defaultMessage: 'Description',
|
||||
},
|
||||
actionsButtonLabel: {
|
||||
id: 'course-authoring.taxonomy-detail.action.button.label',
|
||||
defaultMessage: 'Actions',
|
||||
},
|
||||
actionsButtonAlt: {
|
||||
id: 'course-authoring.taxonomy-detail.action.button.alt',
|
||||
defaultMessage: '{name} actions',
|
||||
},
|
||||
exportMenu: {
|
||||
id: 'course-authoring.taxonomy-detail.action.export',
|
||||
defaultMessage: 'Export',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
Reference in New Issue
Block a user