Skip to content

Commit

Permalink
feat: Only fetch tags when dropdowns are opened
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Nov 14, 2023
1 parent f8cbc91 commit 811c874
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 87 deletions.
26 changes: 5 additions & 21 deletions src/content-tags-drawer/ContentTagsCollapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
<div className="taxonomy-tags-collapsible-div">
<Collapsible title={name} styling="card-lg" className="taxonomy-tags-collapsible">
Expand Down Expand Up @@ -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) => (
<ContentTagsDropDownSelector
key={`selector-${taxonomyTag.value}`}
taxonomyId={id}
taxonomyTag={taxonomyTag}
level={0}
useTaxonomyTagsData={useTaxonomyTagsData}
/>
))}
<ContentTagsDropDownSelector
key={`selector-${id}`}
taxonomyId={id}
level={0}
/>
</SelectableBox.Set>
</div>
</ModalPopup>
Expand Down
129 changes: 86 additions & 43 deletions src/content-tags-drawer/ContentTagsDropDownSelector.jsx
Original file line number Diff line number Diff line change
@@ -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) => {

Check warning on line 28 in src/content-tags-drawer/ContentTagsDropDownSelector.jsx

View check run for this annotation

Codecov / codecov/patch

src/content-tags-drawer/ContentTagsDropDownSelector.jsx#L28

Added line #L28 was not covered by tests
if (index === i) {
return dropdownState === 0 ? 1 : 0;
}
return dropdownState;

Check warning on line 32 in src/content-tags-drawer/ContentTagsDropDownSelector.jsx

View check run for this annotation

Codecov / codecov/patch

src/content-tags-drawer/ContentTagsDropDownSelector.jsx#L32

Added line #L32 was not covered by tests
});

setDropdownStates(updatedState);

Check warning on line 35 in src/content-tags-drawer/ContentTagsDropDownSelector.jsx

View check run for this annotation

Codecov / codecov/patch

src/content-tags-drawer/ContentTagsDropDownSelector.jsx#L35

Added line #L35 was not covered by tests
};

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 (
<div style={{ display: 'flex', flexDirection: 'column', paddingLeft: `${level * 1}rem` }}>
<div style={{ display: 'flex' }}>
<SelectableBox
inputHidden={false}
type="checkbox"
className="pgn__selectable_box taxonomy-tags-selectable-box"
aria-label={`${taxonomyTag.value} checkbox`}
>
{taxonomyTag.value}
</SelectableBox>
{ taxonomyTag.subTagsUrl
&& (
<div className="taxonomy-tags-arrow-drop-down" data-link={taxonomyTag.subTagsUrl}>
<Icon
src={isOpen ? ArrowDropUp : ArrowDropDown}
onClick={clickAndEnterHandler}
tabIndex="0"
onKeyPress={(event) => (event.key === 'Enter' ? clickAndEnterHandler() : null)}
/>
</div>
isTaxonomyTagsLoaded && taxonomyTagsData
? taxonomyTagsData.results.map((taxonomyTag, i) => (
<div style={{ display: 'flex', flexDirection: 'column', paddingLeft: `${level * 1}rem` }} key={`selector-div-${taxonomyTag.value}`}>
<div style={{ display: 'flex' }}>
<SelectableBox
inputHidden={false}
type="checkbox"
className="pgn__selectable_box taxonomy-tags-selectable-box"
aria-label={`${taxonomyTag.value} checkbox`}
>
{taxonomyTag.value}
</SelectableBox>
{ taxonomyTag.subTagsUrl
&& (
<div className="taxonomy-tags-arrow-drop-down" data-link={taxonomyTag.subTagsUrl}>
<Icon
src={isOpen(i) ? ArrowDropUp : ArrowDropDown}
onClick={() => clickAndEnterHandler(i)}

Check warning on line 71 in src/content-tags-drawer/ContentTagsDropDownSelector.jsx

View check run for this annotation

Codecov / codecov/patch

src/content-tags-drawer/ContentTagsDropDownSelector.jsx#L71

Added line #L71 was not covered by tests
tabIndex="0"
onKeyPress={(event) => (event.key === 'Enter' ? clickAndEnterHandler(i) : null)}
/>
</div>
)}
</div>

{ taxonomyTag.subTagsUrl && isOpen(i) && (
<ContentTagsDropDownSelector

Check warning on line 80 in src/content-tags-drawer/ContentTagsDropDownSelector.jsx

View check run for this annotation

Codecov / codecov/patch

src/content-tags-drawer/ContentTagsDropDownSelector.jsx#L80

Added line #L80 was not covered by tests
key={`selector-${taxonomyTag.value}`}
taxonomyId={taxonomyId}
subTagsUrl={taxonomyTag.subTagsUrl}
level={level + 1}
/>
)}
</div>

{ taxonomyTag.subTagsUrl && isOpen && isTaxonomyTagsLoaded && taxonomyTagsData
&& taxonomyTagsData.results.map((childTag) => (
<ContentTagsDropDownSelector
key={`selector-${childTag.value}`}
taxonomyId={taxonomyId}
taxonomyTag={childTag}
level={level + 1}
useTaxonomyTagsData={useTaxonomyTagsData}
</div>
))
: (
<div className="d-flex justify-content-center align-items-center flex-column">
<Spinner
animation="border"
size="xl"
screenReaderText={intl.formatMessage(messages.loadingTagsDropdownMessage)}
/>
))}

</div>
</div>
)
);
};

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;
60 changes: 37 additions & 23 deletions src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => (
<IntlProvider locale="en" messages={{}}>
<ContentTagsDropDownSelector
taxonomyId={taxonomyId}
taxonomyTag={taxonomyTag}
level={level}
useTaxonomyTagsData={useTaxonomyTagsData}
subTagsUrl={subTagsUrl}
/>
</IntlProvider>
);

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('<ContentTagsDropDownSelector />', () => {
it('should render taxonomy tags drop down selector loading with spinner', async () => {
useIsTaxonomyTagsDataLoaded.mockReturnValue(false);
await act(async () => {
const { getByRole } = render(
<TaxonomyTagsDropDownSelectorComponent
taxonomyId={data.taxonomyId}
level={data.level}
/>,
);
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
});
});

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(
<TaxonomyTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
taxonomyTag={data.taxonomyTag}
level={data.level}
useTaxonomyTagsData={data.useTaxonomyTagsData}
/>,
);
expect(getByText('Tag 1')).toBeInTheDocument();
Expand All @@ -65,17 +75,21 @@ describe('<ContentTagsDropDownSelector />', () => {
});

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(
<TaxonomyTagsDropDownSelectorComponent
taxonomyId={data.taxonomyId}
taxonomyTag={data.taxonomyTag}
level={data.level}
useTaxonomyTagsData={data.useTaxonomyTagsData}
/>,
);
expect(getByText('Tag 1')).toBeInTheDocument();
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
});
Expand Down
4 changes: 4 additions & 0 deletions src/content-tags-drawer/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 811c874

Please sign in to comment.