From 811c8746c9e61e8b8536066cb50f87ba90d4760e Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Tue, 14 Nov 2023 14:42:44 +0300 Subject: [PATCH] feat: Only fetch tags when dropdowns are opened --- .../ContentTagsCollapsible.jsx | 26 +--- .../ContentTagsDropDownSelector.jsx | 129 ++++++++++++------ .../ContentTagsDropDownSelector.test.jsx | 60 ++++---- src/content-tags-drawer/messages.js | 4 + 4 files changed, 132 insertions(+), 87 deletions(-) diff --git a/src/content-tags-drawer/ContentTagsCollapsible.jsx b/src/content-tags-drawer/ContentTagsCollapsible.jsx index 22fc37dc8a..65099e88fb 100644 --- a/src/content-tags-drawer/ContentTagsCollapsible.jsx +++ b/src/content-tags-drawer/ContentTagsCollapsible.jsx @@ -14,7 +14,6 @@ import messages from './messages'; import './ContentTagsCollapsible.scss'; import ContentTagsDropDownSelector from './ContentTagsDropDownSelector'; -import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './api/hooks/selectors'; import ContentTagsTree from './ContentTagsTree'; @@ -45,14 +44,6 @@ const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => { const [isOpen, open, close] = useToggle(false); const [target, setTarget] = React.useState(null); - const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => { - const taxonomyTagsData = useTaxonomyTagsDataResponse(taxonomyId, fullPathProvided); - const isTaxonomyTagsLoaded = useIsTaxonomyTagsDataLoaded(taxonomyId, fullPathProvided); - return { taxonomyTagsData, isTaxonomyTagsLoaded }; - }; - - const { taxonomyTagsData, isTaxonomyTagsLoaded } = useTaxonomyTagsData(id); - return (
@@ -85,18 +76,11 @@ const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => { ariaLabel="taxonomy tags selection" className="taxonomy-tags-selectable-box-set" > - {/* TODO: Move the loading logic for taxonomy tags inside the ContentTagsDropDownSelector component - * to avoid unnecessary API calls if they selector is not opened - */} - {isTaxonomyTagsLoaded && taxonomyTagsData && taxonomyTagsData.results.map((taxonomyTag) => ( - - ))} +
diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx index 7240cdc08b..33162a0776 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx @@ -1,69 +1,112 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { SelectableBox, Icon, - useToggle, + Spinner, } from '@edx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { ArrowDropDown, ArrowDropUp } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; +import messages from './messages'; import './ContentTagsDropDownSelector.scss'; +import { useTaxonomyTagsDataResponse, useIsTaxonomyTagsDataLoaded } from './api/hooks/selectors'; + const ContentTagsDropDownSelector = ({ - taxonomyId, taxonomyTag, level, useTaxonomyTagsData, + taxonomyId, level, subTagsUrl, }) => { - const [isOpen, open, close] = useToggle(false); + const intl = useIntl(); + // This array represents the states of the dropdowns on this level + // Each index contains whether the dropdown is open/closed + // O === Closed + // 1 === Open + const [dropdownStates, setDropdownStates] = useState([]); + + const isOpen = (i) => (dropdownStates[i] === 1); + + const clickAndEnterHandler = (index) => { + const updatedState = dropdownStates.map((dropdownState, i) => { + if (index === i) { + return dropdownState === 0 ? 1 : 0; + } + return dropdownState; + }); + + setDropdownStates(updatedState); + }; + + const useTaxonomyTagsData = (id, fullPathProvided) => { + const taxonomyTagsData = useTaxonomyTagsDataResponse(id, fullPathProvided); + const isTaxonomyTagsLoaded = useIsTaxonomyTagsDataLoaded(id, fullPathProvided); + return { taxonomyTagsData, isTaxonomyTagsLoaded }; + }; - const clickAndEnterHandler = () => (isOpen ? close() : open()); + const { taxonomyTagsData, isTaxonomyTagsLoaded } = useTaxonomyTagsData(taxonomyId, subTagsUrl); - const { taxonomyTagsData, isTaxonomyTagsLoaded } = useTaxonomyTagsData(taxonomyId, taxonomyTag.subTagsUrl); + useEffect(() => { + if (isTaxonomyTagsLoaded && taxonomyTagsData) { + // When the data first loads, all the dropdowns are initially closed + setDropdownStates(new Array(taxonomyTagsData.results.length).fill(0)); + } + }, [isTaxonomyTagsLoaded, taxonomyTagsData]); return ( -
-
- - {taxonomyTag.value} - - { taxonomyTag.subTagsUrl - && ( -
- (event.key === 'Enter' ? clickAndEnterHandler() : null)} - /> -
+ isTaxonomyTagsLoaded && taxonomyTagsData + ? taxonomyTagsData.results.map((taxonomyTag, i) => ( +
+
+ + {taxonomyTag.value} + + { taxonomyTag.subTagsUrl + && ( +
+ clickAndEnterHandler(i)} + tabIndex="0" + onKeyPress={(event) => (event.key === 'Enter' ? clickAndEnterHandler(i) : null)} + /> +
+ )} +
+ + { taxonomyTag.subTagsUrl && isOpen(i) && ( + )} -
- { taxonomyTag.subTagsUrl && isOpen && isTaxonomyTagsLoaded && taxonomyTagsData - && taxonomyTagsData.results.map((childTag) => ( - + )) + : ( +
+ - ))} - -
+
+ ) ); }; +ContentTagsDropDownSelector.defaultProps = { + subTagsUrl: undefined, +}; + ContentTagsDropDownSelector.propTypes = { taxonomyId: PropTypes.number.isRequired, - taxonomyTag: PropTypes.shape({ - value: PropTypes.string, - subTagsUrl: PropTypes.string, - }).isRequired, level: PropTypes.number.isRequired, - useTaxonomyTagsData: PropTypes.func.isRequired, + subTagsUrl: PropTypes.string, }; export default ContentTagsDropDownSelector; diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx index 2e91dec2c8..100b7cc779 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx @@ -13,50 +13,60 @@ jest.mock('./api/hooks/selectors', () => ({ const data = { taxonomyId: 123, - taxonomyTag: { - value: 'Tag 1', - subTagsUrl: null, - }, level: 0, - useTaxonomyTagsData: (taxonomyId, fullPathProvided) => { - const taxonomyTagsData = useTaxonomyTagsDataResponse(taxonomyId, fullPathProvided); - const isTaxonomyTagsLoaded = useIsTaxonomyTagsDataLoaded(taxonomyId, fullPathProvided); - return { taxonomyTagsData, isTaxonomyTagsLoaded }; - }, }; const TaxonomyTagsDropDownSelectorComponent = ({ - taxonomyId, taxonomyTag, level, useTaxonomyTagsData, + taxonomyId, level, subTagsUrl, }) => ( ); +TaxonomyTagsDropDownSelectorComponent.defaultProps = { + subTagsUrl: undefined, +}; + TaxonomyTagsDropDownSelectorComponent.propTypes = { taxonomyId: PropTypes.number.isRequired, - taxonomyTag: PropTypes.shape({ - value: PropTypes.string, - subTagsUrl: PropTypes.string, - }).isRequired, level: PropTypes.number.isRequired, - useTaxonomyTagsData: PropTypes.func.isRequired, + subTagsUrl: PropTypes.string, }; describe('', () => { + it('should render taxonomy tags drop down selector loading with spinner', async () => { + useIsTaxonomyTagsDataLoaded.mockReturnValue(false); + await act(async () => { + const { getByRole } = render( + , + ); + const spinner = getByRole('status'); + expect(spinner.textContent).toEqual('Loading tags'); // Uses + }); + }); + it('should render taxonomy tags drop down selector with no sub tags', async () => { + useIsTaxonomyTagsDataLoaded.mockReturnValue(true); + useTaxonomyTagsDataResponse.mockReturnValue({ + results: [{ + value: 'Tag 1', + subTagsUrl: null, + }], + }); await act(async () => { const { container, getByText } = render( , ); expect(getByText('Tag 1')).toBeInTheDocument(); @@ -65,17 +75,21 @@ describe('', () => { }); it('should render taxonomy tags drop down selector with sub tags', async () => { - data.taxonomyTag.subTagsUrl = 'https://example.com'; + useIsTaxonomyTagsDataLoaded.mockReturnValue(true); + useTaxonomyTagsDataResponse.mockReturnValue({ + results: [{ + value: 'Tag 2', + subTagsUrl: 'https://example.com', + }], + }); await act(async () => { const { container, getByText } = render( , ); - expect(getByText('Tag 1')).toBeInTheDocument(); + expect(getByText('Tag 2')).toBeInTheDocument(); expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1); }); }); diff --git a/src/content-tags-drawer/messages.js b/src/content-tags-drawer/messages.js index 44b458087d..7eac7cfeba 100644 --- a/src/content-tags-drawer/messages.js +++ b/src/content-tags-drawer/messages.js @@ -13,6 +13,10 @@ const messages = defineMessages({ id: 'course-authoring.content-tags-drawer.spinner.loading', defaultMessage: 'Loading', }, + loadingTagsDropdownMessage: { + id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.spinner.loading', + defaultMessage: 'Loading tags', + }, }); export default messages;