From d1d1ee0d9c25b6cee9452f48ab7b92b80f27d3b2 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Wed, 15 Nov 2023 16:00:59 +0300 Subject: [PATCH] refactor: follow existing structure for reactQuery/APIs --- src/content-tags-drawer/ContentTagsDrawer.jsx | 4 +- .../ContentTagsDrawer.test.jsx | 4 +- .../ContentTagsDropDownSelector.jsx | 2 +- .../ContentTagsDropDownSelector.test.jsx | 2 +- .../__mocks__/contentDataMock.js | 63 +++++++++ .../__mocks__/contentTaxonomyTagsMock.js | 50 +++++++ src/content-tags-drawer/__mocks__/index.js | 3 + .../__mocks__/taxonomyTagsMock.js | 46 +++++++ src/content-tags-drawer/api/hooks/api.js | 44 ------ src/content-tags-drawer/api/hooks/api.test.js | 57 -------- .../api/hooks/selectors.js | 56 -------- .../api/hooks/selectors.test.js | 127 ------------------ src/content-tags-drawer/data/api.js | 42 ++++++ src/content-tags-drawer/data/api.test.js | 71 ++++++++++ src/content-tags-drawer/data/apiHooks.jsx | 111 +++++++++++++++ .../data/apiHooks.test.jsx | 121 +++++++++++++++++ .../{api => data}/types.mjs | 0 17 files changed, 513 insertions(+), 290 deletions(-) create mode 100644 src/content-tags-drawer/__mocks__/contentDataMock.js create mode 100644 src/content-tags-drawer/__mocks__/contentTaxonomyTagsMock.js create mode 100644 src/content-tags-drawer/__mocks__/index.js create mode 100644 src/content-tags-drawer/__mocks__/taxonomyTagsMock.js delete mode 100644 src/content-tags-drawer/api/hooks/api.js delete mode 100644 src/content-tags-drawer/api/hooks/api.test.js delete mode 100644 src/content-tags-drawer/api/hooks/selectors.js delete mode 100644 src/content-tags-drawer/api/hooks/selectors.test.js create mode 100644 src/content-tags-drawer/data/api.js create mode 100644 src/content-tags-drawer/data/api.test.js create mode 100644 src/content-tags-drawer/data/apiHooks.jsx create mode 100644 src/content-tags-drawer/data/apiHooks.test.jsx rename src/content-tags-drawer/{api => data}/types.mjs (100%) diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx index fc2c7530ee..254e646cf5 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.jsx @@ -14,7 +14,7 @@ import { useIsContentTaxonomyTagsDataLoaded, useContentDataResponse, useIsContentDataLoaded, -} from './api/hooks/selectors'; +} from './data/apiHooks'; import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/data/apiHooks'; import Loading from '../generic/Loading'; @@ -80,7 +80,7 @@ const ContentTagsDrawer = () => { // eslint-disable-next-line array-callback-return contentTaxonomies.map((contentTaxonomyTags) => { - const contentTaxonomy = taxonomiesList.find((taxonomy) => taxonomy.id === contentTaxonomyTags.taxonomy_id); + const contentTaxonomy = taxonomiesList.find((taxonomy) => taxonomy.id === contentTaxonomyTags.taxonomyId); if (contentTaxonomy) { contentTaxonomy.contentTags = contentTaxonomyTags.tags; } diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx index 5f91f38caa..719efef46b 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx @@ -8,8 +8,8 @@ import { useIsContentTaxonomyTagsDataLoaded, useContentDataResponse, useIsContentDataLoaded, -} from './api/hooks/selectors'; -import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/api/hooks/selectors'; +} from './data/apiHooks'; +import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/data/apiHooks'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx index 56b4634e8c..5195b478e6 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import messages from './messages'; import './ContentTagsDropDownSelector.scss'; -import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './api/hooks/selectors'; +import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './data/apiHooks'; const ContentTagsDropDownSelector = ({ taxonomyId, level, subTagsUrl, diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx index 100b7cc779..ce5525642f 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx @@ -4,7 +4,7 @@ import { act, render } from '@testing-library/react'; import PropTypes from 'prop-types'; import ContentTagsDropDownSelector from './ContentTagsDropDownSelector'; -import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './api/hooks/selectors'; +import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './data/apiHooks'; jest.mock('./api/hooks/selectors', () => ({ useTaxonomyTagsDataResponse: jest.fn(), diff --git a/src/content-tags-drawer/__mocks__/contentDataMock.js b/src/content-tags-drawer/__mocks__/contentDataMock.js new file mode 100644 index 0000000000..292efc38d0 --- /dev/null +++ b/src/content-tags-drawer/__mocks__/contentDataMock.js @@ -0,0 +1,63 @@ +module.exports = { + id: 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab', + displayName: 'Unit 1.1.2', + category: 'vertical', + hasChildren: true, + editedOn: 'Nov 12, 2023 at 09:53 UTC', + published: false, + publishedOn: null, + studioUrl: '/container/block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab', + releasedToStudents: false, + releaseDate: null, + visibilityState: 'needs_attention', + hasExplicitStaffLock: false, + start: '2030-01-01T00:00:00Z', + graded: false, + dueDate: '', + due: null, + relativeWeeksDue: null, + format: null, + courseGraders: [ + 'Homework', + 'Lab', + 'Midterm Exam', + 'Final Exam', + ], + hasChanges: true, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatoryMessage: null, + groupAccess: {}, + userPartitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + showCorrectness: 'always', + discussionEnabled: true, + ancestorHasStaffLock: false, + taxonomyTagsWidgetUrl: 'http://localhost:2001/tagging/components/widget/', + staffOnlyMessage: false, + enableCopyPasteUnits: true, + useTaggingTaxonomyListPage: true, + hasPartitionGroupComponents: false, + userPartitionInfo: { + selectablePartitions: [], + selectedPartitionIndex: -1, + selectedGroupsLabel: '', + }, +}; diff --git a/src/content-tags-drawer/__mocks__/contentTaxonomyTagsMock.js b/src/content-tags-drawer/__mocks__/contentTaxonomyTagsMock.js new file mode 100644 index 0000000000..2e8aa0bea7 --- /dev/null +++ b/src/content-tags-drawer/__mocks__/contentTaxonomyTagsMock.js @@ -0,0 +1,50 @@ +module.exports = { + 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b': { + taxonomies: [ + { + name: 'FlatTaxonomy', + taxonomyId: 3, + editable: true, + tags: [ + { + value: 'flat taxonomy tag 3856', + lineage: [ + 'flat taxonomy tag 3856', + ], + }, + ], + }, + { + name: 'HierarchicalTaxonomy', + taxonomyId: 4, + editable: true, + tags: [ + { + value: 'hierarchical taxonomy tag 1.7.59', + lineage: [ + 'hierarchical taxonomy tag 1', + 'hierarchical taxonomy tag 1.7', + 'hierarchical taxonomy tag 1.7.59', + ], + }, + { + value: 'hierarchical taxonomy tag 2.13.46', + lineage: [ + 'hierarchical taxonomy tag 2', + 'hierarchical taxonomy tag 2.13', + 'hierarchical taxonomy tag 2.13.46', + ], + }, + { + value: 'hierarchical taxonomy tag 3.4.50', + lineage: [ + 'hierarchical taxonomy tag 3', + 'hierarchical taxonomy tag 3.4', + 'hierarchical taxonomy tag 3.4.50', + ], + }, + ], + }, + ], + }, +}; diff --git a/src/content-tags-drawer/__mocks__/index.js b/src/content-tags-drawer/__mocks__/index.js new file mode 100644 index 0000000000..b09fc5d3ab --- /dev/null +++ b/src/content-tags-drawer/__mocks__/index.js @@ -0,0 +1,3 @@ +export { default as taxonomyTagsMock } from './taxonomyTagsMock'; +export { default as contentTaxonomyTagsMock } from './contentTaxonomyTagsMock'; +export { default as contentDataMock } from './contentDataMock'; diff --git a/src/content-tags-drawer/__mocks__/taxonomyTagsMock.js b/src/content-tags-drawer/__mocks__/taxonomyTagsMock.js new file mode 100644 index 0000000000..0b2bc714c8 --- /dev/null +++ b/src/content-tags-drawer/__mocks__/taxonomyTagsMock.js @@ -0,0 +1,46 @@ +module.exports = { + next: null, + previous: null, + count: 4, + numPages: 1, + currentPage: 1, + start: 0, + results: [ + { + value: 'tag 1', + externalId: null, + childCount: 16, + depth: 0, + parentValue: null, + id: 635951, + subTagsUrl: 'http://localhost:18010/api/content_tagging/v1/taxonomies/4/tags/?parent_tag=tag%201', + }, + { + value: 'tag 2', + externalId: null, + childCount: 16, + depth: 0, + parentValue: null, + id: 636992, + subTagsUrl: 'http://localhost:18010/api/content_tagging/v1/taxonomies/4/tags/?parent_tag=tag%202', + }, + { + value: 'tag 3', + externalId: null, + childCount: 16, + depth: 0, + parentValue: null, + id: 638033, + subTagsUrl: 'http://localhost:18010/api/content_tagging/v1/taxonomies/4/tags/?parent_tag=tag%203', + }, + { + value: 'tag 4', + externalId: null, + childCount: 16, + depth: 0, + parentValue: null, + id: 639074, + subTagsUrl: 'http://localhost:18010/api/content_tagging/v1/taxonomies/4/tags/?parent_tag=tag%204', + }, + ], +}; diff --git a/src/content-tags-drawer/api/hooks/api.js b/src/content-tags-drawer/api/hooks/api.js deleted file mode 100644 index d0969f0fa3..0000000000 --- a/src/content-tags-drawer/api/hooks/api.js +++ /dev/null @@ -1,44 +0,0 @@ -// @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 getTaxonomyTagsApiUrl = (taxonomyId) => new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl()).href; -const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href; -const getContentDataApiUrl = (contentId) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href; - -/** - * @returns {import("../types.mjs").UseQueryResult} - */ -export const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => ( - useQuery({ - queryKey: [`taxonomyTags${ fullPathProvided || taxonomyId }`], - queryFn: () => getAuthenticatedHttpClient().get( - fullPathProvided ? new URL(`${fullPathProvided}`) : getTaxonomyTagsApiUrl(taxonomyId), - ) - .then(camelCaseObject), - }) -); - -/** - * @returns {import("../types.mjs").UseQueryResult} - */ -export const useContentTaxonomyTagsData = (contentId) => ( - useQuery({ - queryKey: ['contentTaxonomyTags'], - queryFn: () => getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId)) - .then((response) => response.data[contentId]), - }) -); - -/** - * @returns {import("../types.mjs").UseQueryResult} - */ -export const useContentData = (contentId) => ( - useQuery({ - queryKey: ['contentData'], - queryFn: () => getAuthenticatedHttpClient().get(getContentDataApiUrl(contentId)) - .then(camelCaseObject), - }) -); diff --git a/src/content-tags-drawer/api/hooks/api.test.js b/src/content-tags-drawer/api/hooks/api.test.js deleted file mode 100644 index ffdd20638f..0000000000 --- a/src/content-tags-drawer/api/hooks/api.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useTaxonomyTagsData, useContentTaxonomyTagsData, useContentData } from './api'; - -jest.mock('@tanstack/react-query', () => ({ - useQuery: jest.fn(), -})); - -jest.mock('@edx/frontend-platform/auth', () => ({ - getAuthenticatedHttpClient: jest.fn(), -})); - -describe('content taxonomy tags API: useTaxonomyTagsData', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should call useQuery with the correct parameters', () => { - const taxonomyId = 'testTaxonomy'; - const fullPathProvided = ''; - useTaxonomyTagsData(taxonomyId, fullPathProvided); - - expect(useQuery).toHaveBeenCalledWith({ - queryKey: [`taxonomyTags${taxonomyId}${fullPathProvided}`], - queryFn: expect.any(Function), - }); - }); -}); - -describe('content taxonomy tags API: useContentTaxonomyTagsData', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should call useQuery with the correct parameters', () => { - useContentTaxonomyTagsData('testContentId'); - - expect(useQuery).toHaveBeenCalledWith({ - queryKey: ['contentTaxonomyTags'], - queryFn: expect.any(Function), - }); - }); -}); - -describe('content taxonomy tags API: useContentData', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should call useQuery with the correct parameters', () => { - useContentData('testContentId'); - - expect(useQuery).toHaveBeenCalledWith({ - queryKey: ['contentData'], - queryFn: expect.any(Function), - }); - }); -}); diff --git a/src/content-tags-drawer/api/hooks/selectors.js b/src/content-tags-drawer/api/hooks/selectors.js deleted file mode 100644 index c4605491c3..0000000000 --- a/src/content-tags-drawer/api/hooks/selectors.js +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-check -import { useTaxonomyTagsData, useContentTaxonomyTagsData, useContentData } from './api'; - -/** - * @returns {import("../types.mjs").TaxonomyTagsData | undefined} - */ -export const useTaxonomyTagsDataResponse = (taxonomyId, fullPathProvided) => { - const response = useTaxonomyTagsData(taxonomyId, fullPathProvided); - if (response.status === 'success') { - return response.data.data; - } - return undefined; -}; - -/** - * @returns {boolean} - */ -export const useIsTaxonomyTagsDataLoaded = (taxonomyId, fullPathProvided) => ( - useTaxonomyTagsData(taxonomyId, fullPathProvided).status === 'success' -); - -/** - * @returns {import("../types.mjs").ContentTaxonomyTagsData | undefined} - */ -export const useContentTaxonomyTagsDataResponse = (contentId) => { - const response = useContentTaxonomyTagsData(contentId); - if (response.status === 'success') { - return response.data; - } - return undefined; -}; - -/** - * @returns {boolean} - */ -export const useIsContentTaxonomyTagsDataLoaded = (contentId) => ( - useContentTaxonomyTagsData(contentId).status === 'success' -); - -/** - * @returns {import("../types.mjs").ContentData | undefined} - */ -export const useContentDataResponse = (contentId) => { - const response = useContentData(contentId); - if (response.status === 'success') { - return response.data.data; - } - return undefined; -}; - -/** - * @returns {boolean} - */ -export const useIsContentDataLoaded = (contentId) => ( - useContentData(contentId).status === 'success' -); diff --git a/src/content-tags-drawer/api/hooks/selectors.test.js b/src/content-tags-drawer/api/hooks/selectors.test.js deleted file mode 100644 index c359188b1b..0000000000 --- a/src/content-tags-drawer/api/hooks/selectors.test.js +++ /dev/null @@ -1,127 +0,0 @@ -import { - useTaxonomyTagsDataResponse, - useIsTaxonomyTagsDataLoaded, - useContentTaxonomyTagsDataResponse, - useIsContentTaxonomyTagsDataLoaded, - useContentDataResponse, - useIsContentDataLoaded, -} from './selectors'; -import { - useTaxonomyTagsData, - useContentTaxonomyTagsData, - useContentData, -} from './api'; - -jest.mock('./api', () => ({ - useTaxonomyTagsData: jest.fn(), - useContentTaxonomyTagsData: jest.fn(), - useContentData: jest.fn(), -})); - -describe('useTaxonomyTagsDataResponse', () => { - it('should return data when status is success', () => { - useTaxonomyTagsData.mockReturnValueOnce({ status: 'success', data: { data: 'data' } }); - - const result = useTaxonomyTagsDataResponse(); - - expect(result).toEqual('data'); - }); - - it('should return undefined when status is not success', () => { - useTaxonomyTagsData.mockReturnValueOnce({ status: 'error' }); - - const result = useTaxonomyTagsDataResponse(); - - expect(result).toBeUndefined(); - }); -}); - -describe('useIsTaxonomyTagsDataLoaded', () => { - it('should return true when status is success', () => { - useTaxonomyTagsData.mockReturnValueOnce({ status: 'success' }); - - const result = useIsTaxonomyTagsDataLoaded(); - - expect(result).toBe(true); - }); - - it('should return false when status is not success', () => { - useTaxonomyTagsData.mockReturnValueOnce({ status: 'error' }); - - const result = useIsTaxonomyTagsDataLoaded(); - - expect(result).toBe(false); - }); -}); - -describe('useContentTaxonomyTagsDataResponse', () => { - it('should return data when status is success', () => { - useContentTaxonomyTagsData.mockReturnValueOnce({ status: 'success', data: 'data' }); - - const result = useContentTaxonomyTagsDataResponse(); - - expect(result).toEqual('data'); - }); - - it('should return undefined when status is not success', () => { - useContentTaxonomyTagsData.mockReturnValueOnce({ status: 'error' }); - - const result = useContentTaxonomyTagsDataResponse(); - - expect(result).toBeUndefined(); - }); -}); - -describe('useIsContentTaxonomyTagsDataLoaded', () => { - it('should return true when status is success', () => { - useContentTaxonomyTagsData.mockReturnValueOnce({ status: 'success' }); - - const result = useIsContentTaxonomyTagsDataLoaded(); - - expect(result).toBe(true); - }); - - it('should return false when status is not success', () => { - useContentTaxonomyTagsData.mockReturnValueOnce({ status: 'error' }); - - const result = useIsContentTaxonomyTagsDataLoaded(); - - expect(result).toBe(false); - }); -}); - -describe('useContentDataResponse', () => { - it('should return data when status is success', () => { - useContentData.mockReturnValueOnce({ status: 'success', data: { data: 'data' } }); - - const result = useContentDataResponse(); - - expect(result).toEqual('data'); - }); - - it('should return undefined when status is not success', () => { - useContentData.mockReturnValueOnce({ status: 'error' }); - - const result = useContentDataResponse(); - - expect(result).toBeUndefined(); - }); -}); - -describe('useIsContentDataLoaded', () => { - it('should return true when status is success', () => { - useContentData.mockReturnValueOnce({ status: 'success' }); - - const result = useIsContentDataLoaded(); - - expect(result).toBe(true); - }); - - it('should return false when status is not success', () => { - useContentData.mockReturnValueOnce({ status: 'error' }); - - const result = useIsContentDataLoaded(); - - expect(result).toBe(false); - }); -}); diff --git a/src/content-tags-drawer/data/api.js b/src/content-tags-drawer/data/api.js new file mode 100644 index 0000000000..e63b3d0842 --- /dev/null +++ b/src/content-tags-drawer/data/api.js @@ -0,0 +1,42 @@ +// @ts-check +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; +export const getTaxonomyTagsApiUrl = (taxonomyId) => new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl()).href; +export const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href; +export const getContentDataApiUrl = (contentId) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href; + +/** + * Get all tags that belong to taxonomy. + * @param {string} 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} + */ +export async function getTaxonomyTagsData(taxonomyId, fullPathProvided) { + const { data } = await getAuthenticatedHttpClient().get( + fullPathProvided ? new URL(`${fullPathProvided}`) : getTaxonomyTagsApiUrl(taxonomyId), + ); + return camelCaseObject(data); +} + +/** + * 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} + */ +export async function getContentTaxonomyTagsData(contentId) { + const { data } = await getAuthenticatedHttpClient().get(getContentTaxonomyTagsApiUrl(contentId)); + return camelCaseObject(data[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} + */ +export async function getContentData(contentId) { + const { data } = await getAuthenticatedHttpClient().get(getContentDataApiUrl(contentId)); + return camelCaseObject(data); +} diff --git a/src/content-tags-drawer/data/api.test.js b/src/content-tags-drawer/data/api.test.js new file mode 100644 index 0000000000..ffe19ab960 --- /dev/null +++ b/src/content-tags-drawer/data/api.test.js @@ -0,0 +1,71 @@ +import MockAdapter from 'axios-mock-adapter'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +import { taxonomyTagsMock, contentTaxonomyTagsMock, contentDataMock } from '../__mocks__'; + +import { + getTaxonomyTagsApiUrl, + getContentTaxonomyTagsApiUrl, + getContentDataApiUrl, + getTaxonomyTagsData, + getContentTaxonomyTagsData, + getContentData, +} from './api'; + +let axiosMock; + +describe('content tags drawer api calls', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should get taxonomy tags data', async () => { + const taxonomyId = '123'; + axiosMock.onGet().reply(200, taxonomyTagsMock); + const result = await getTaxonomyTagsData(taxonomyId); + + expect(axiosMock.history.get[0].url).toEqual(getTaxonomyTagsApiUrl(taxonomyId)); + expect(result).toEqual(taxonomyTagsMock); + }); + + it('should get taxonomy tags data with fullPathProvided', async () => { + const taxonomyId = '123'; + const fullPathProvided = 'http://example.com/'; + axiosMock.onGet().reply(200, taxonomyTagsMock); + const result = await getTaxonomyTagsData(taxonomyId, fullPathProvided); + + expect(axiosMock.history.get[0].url).toEqual(new URL(`${fullPathProvided}`)); + expect(result).toEqual(taxonomyTagsMock); + }); + + it('should get content taxonomy tags data', async () => { + const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'; + axiosMock.onGet(getContentTaxonomyTagsApiUrl(contentId)).reply(200, contentTaxonomyTagsMock); + const result = await getContentTaxonomyTagsData(contentId); + + expect(axiosMock.history.get[0].url).toEqual(getContentTaxonomyTagsApiUrl(contentId)); + expect(result).toEqual(contentTaxonomyTagsMock[contentId]); + }); + + it('should get content data', async () => { + const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b'; + axiosMock.onGet(getContentDataApiUrl(contentId)).reply(200, contentDataMock); + const result = await getContentData(contentId); + + expect(axiosMock.history.get[0].url).toEqual(getContentDataApiUrl(contentId)); + expect(result).toEqual(contentDataMock); + }); +}); diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx new file mode 100644 index 0000000000..099c88129d --- /dev/null +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -0,0 +1,111 @@ +// @ts-check +import { useQuery } from '@tanstack/react-query'; +import { getTaxonomyTagsData, getContentTaxonomyTagsData, getContentData } from './api'; + +/** + * Builds the query to get the taxonomy tags + * @param {string} 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("./types.mjs").UseQueryResult} + */ +const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => ( + useQuery({ + queryKey: [`taxonomyTags${ fullPathProvided || taxonomyId }`], + queryFn: () => getTaxonomyTagsData(taxonomyId, fullPathProvided), + }) +); + +/** + * Gets the taxonomy tags data + * @param {string} 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("./types.mjs").TaxonomyTagsData | undefined} + */ +export const useTaxonomyTagsDataResponse = (taxonomyId, fullPathProvided) => { + const response = useTaxonomyTagsData(taxonomyId, fullPathProvided); + if (response.status === 'success') { + return response.data; + } + return undefined; +}; + +/** + * Returns the status of the taxonomy tags query + * @param {string} 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 {boolean} + */ +export const useIsTaxonomyTagsDataLoaded = (taxonomyId, fullPathProvided) => ( + useTaxonomyTagsData(taxonomyId, fullPathProvided).status === 'success' +); + +/** + * 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("./types.mjs").UseQueryResult} + */ +const useContentTaxonomyTagsData = (contentId) => ( + useQuery({ + queryKey: ['contentTaxonomyTags'], + queryFn: () => getContentTaxonomyTagsData(contentId), + }) +); + +/** + * Gets 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("./types.mjs").ContentTaxonomyTagsData | undefined} + */ +export const useContentTaxonomyTagsDataResponse = (contentId) => { + const response = useContentTaxonomyTagsData(contentId); + if (response.status === 'success') { + return response.data; + } + return undefined; +}; + +/** + * Gets the status of the content taxonomy tags query + * @param {string} contentId The id of the content object to fetch the applied tags for + * @returns {boolean} + */ +export const useIsContentTaxonomyTagsDataLoaded = (contentId) => ( + useContentTaxonomyTagsData(contentId).status === 'success' +); + +/** + * Builds the query to get meta data about the content object + * @param {string} contentId The id of the content object (unit/component) + * @returns {import("./types.mjs").UseQueryResult} + */ +const useContentData = (contentId) => ( + useQuery({ + queryKey: ['contentData'], + queryFn: () => getContentData(contentId), + }) +); + +/** + * Gets the information about the content object + * @param {string} contentId The id of the content object (unit/component) + * @returns {import("./types.mjs").ContentData | undefined} + */ +export const useContentDataResponse = (contentId) => { + const response = useContentData(contentId); + if (response.status === 'success') { + return response.data; + } + return undefined; +}; + +/** + * Gets the status of the content data query + * @param {string} contentId The id of the content object (unit/component) + * @returns {boolean} + */ +export const useIsContentDataLoaded = (contentId) => ( + useContentData(contentId).status === 'success' +); diff --git a/src/content-tags-drawer/data/apiHooks.test.jsx b/src/content-tags-drawer/data/apiHooks.test.jsx new file mode 100644 index 0000000000..f969782adc --- /dev/null +++ b/src/content-tags-drawer/data/apiHooks.test.jsx @@ -0,0 +1,121 @@ +import { useQuery } from '@tanstack/react-query'; +import { + useTaxonomyTagsDataResponse, + useIsTaxonomyTagsDataLoaded, + useContentTaxonomyTagsDataResponse, + useIsContentTaxonomyTagsDataLoaded, + useContentDataResponse, + useIsContentDataLoaded, +} from './apiHooks'; + +jest.mock('@tanstack/react-query', () => ({ + useQuery: jest.fn(), +})); + +describe('useTaxonomyTagsDataResponse', () => { + it('should return data when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success', data: { data: 'data' } }); + const taxonomyId = '123'; + const result = useTaxonomyTagsDataResponse(taxonomyId); + + expect(result).toEqual({ data: 'data' }); + }); + + it('should return undefined when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const taxonomyId = '123'; + const result = useTaxonomyTagsDataResponse(taxonomyId); + + expect(result).toBeUndefined(); + }); +}); + +describe('useIsTaxonomyTagsDataLoaded', () => { + it('should return true when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success' }); + const taxonomyId = '123'; + const result = useIsTaxonomyTagsDataLoaded(taxonomyId); + + expect(result).toBe(true); + }); + + it('should return false when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const taxonomyId = '123'; + const result = useIsTaxonomyTagsDataLoaded(taxonomyId); + + expect(result).toBe(false); + }); +}); + +describe('useContentTaxonomyTagsDataResponse', () => { + it('should return data when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success', data: { data: 'data' } }); + const contentId = '123'; + const result = useContentTaxonomyTagsDataResponse(contentId); + + expect(result).toEqual({ data: 'data' }); + }); + + it('should return undefined when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const contentId = '123'; + const result = useContentTaxonomyTagsDataResponse(contentId); + + expect(result).toBeUndefined(); + }); +}); + +describe('useIsContentTaxonomyTagsDataLoaded', () => { + it('should return true when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success' }); + const contentId = '123'; + const result = useIsContentTaxonomyTagsDataLoaded(contentId); + + expect(result).toBe(true); + }); + + it('should return false when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const contentId = '123'; + const result = useIsContentTaxonomyTagsDataLoaded(contentId); + + expect(result).toBe(false); + }); +}); + +describe('useContentDataResponse', () => { + it('should return data when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success', data: { data: 'data' } }); + const contentId = '123'; + const result = useContentDataResponse(contentId); + + expect(result).toEqual({ data: 'data' }); + }); + + it('should return undefined when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const contentId = '123'; + const result = useContentDataResponse(contentId); + + expect(result).toBeUndefined(); + }); +}); + +describe('useIsContentDataLoaded', () => { + it('should return true when status is success', () => { + useQuery.mockReturnValueOnce({ status: 'success' }); + const contentId = '123'; + const result = useIsContentDataLoaded(contentId); + + expect(result).toBe(true); + }); + + it('should return false when status is not success', () => { + useQuery.mockReturnValueOnce({ status: 'error' }); + const contentId = '123'; + const result = useIsContentDataLoaded(contentId); + + expect(result).toBe(false); + }); +}); diff --git a/src/content-tags-drawer/api/types.mjs b/src/content-tags-drawer/data/types.mjs similarity index 100% rename from src/content-tags-drawer/api/types.mjs rename to src/content-tags-drawer/data/types.mjs