-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This implements a side drawer widget for content taxonomy tags. It includes displaying the object's tags, along with their lineage (ancestor tags) data. It also implements the listing the available taxonomy tags (including nesting ones) to select from to apply to this unit. Note: The editing of tags (adding/removing) will be added in a future PR. * feat: Add initial UnitTaxonomyTagsDrawer widget * feat: Add fetching unit taxonomy tags from backend * feat: Add fetching/group tags with taxonomies * feat: Add fetch Unit data and display name * feat: Add Taxonomy Tags dropdown selector * feat: Add TagBubble for tag styling * chore: Add distinct keys to elements + remove logs * feat: Add close drawer with ESC- keypress * feat: Make dropdown selectors keyboard accessible * chore: Fix issues causing validation to fail * test: Add coverage tests for UnitTaxonomyDrawer * feat: Incorporate tags lineage data from API * refactor: Remove/replace deprecated injectIntl * test: Remove redux store related code + fix warnings * feat: Use <Loading /> instead of loading string * docs: Add docs string to TaxonomyTagsCollapsible * feat: Use <Spinner/> to allow mutiple loading to show * feat: Rename UnitTaxonomyTagDrawer -> ContentTagsDrawer * feat: Add ContentTagsTree component to render Tags * feat: Only fetch tags when dropdowns are opened * refactor: Simply dropdown close/open states * feat: Use built in class styles instead of custom * feat: Replace hardcoded values with scss variables * refactor: follow existing structure for reactQuery/APIs * feat: Change tag bubble outline color * feat: Add TagOutlineIcon for implicit tags * feat: Make aria label internationalized * feat: Replace custom styles with builtin classes * fix: Fix bug with closing drawer * refactor: Simplify content tags fetching code * refactor: Simplify getTaxonomyListApiUrl
- Loading branch information
1 parent
9b053de
commit 375006d
Showing
32 changed files
with
1,686 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import React from 'react'; | ||
import { | ||
Badge, | ||
Collapsible, | ||
SelectableBox, | ||
Button, | ||
ModalPopup, | ||
useToggle, | ||
} from '@edx/paragon'; | ||
import PropTypes from 'prop-types'; | ||
import classNames from 'classnames'; | ||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; | ||
import messages from './messages'; | ||
import './ContentTagsCollapsible.scss'; | ||
|
||
import ContentTagsDropDownSelector from './ContentTagsDropDownSelector'; | ||
|
||
import ContentTagsTree from './ContentTagsTree'; | ||
|
||
/** | ||
* Collapsible component that holds a Taxonomy along with Tags that belong to it. | ||
* This includes both applied tags and tags that are available to select | ||
* from a dropdown list. | ||
* @param {Object} taxonomyAndTagsData - Object containing Taxonomy meta data along with applied tags | ||
* @param {number} taxonomyAndTagsData.id - id of Taxonomy | ||
* @param {string} taxonomyAndTagsData.name - name of Taxonomy | ||
* @param {string} taxonomyAndTagsData.description - description of Taxonomy | ||
* @param {boolean} taxonomyAndTagsData.enabled - Whether Taxonomy is enabled/disabled | ||
* @param {boolean} taxonomyAndTagsData.allowMultiple - Whether Taxonomy allows multiple tags to be applied | ||
* @param {boolean} taxonomyAndTagsData.allowFreeText - Whether Taxonomy allows free text tags | ||
* @param {boolean} taxonomyAndTagsData.systemDefined - Whether Taxonomy is system defined or authored by user | ||
* @param {boolean} taxonomyAndTagsData.visibleToAuthors - Whether Taxonomy should be visible to object authors | ||
* @param {string[]} taxonomyAndTagsData.orgs - Array of orgs this Taxonomy belongs to | ||
* @param {boolean} taxonomyAndTagsData.allOrgs - Whether Taxonomy belongs to all orgs | ||
* @param {Object[]} taxonomyAndTagsData.contentTags - Array of taxonomy tags that are applied to the content | ||
* @param {string} taxonomyAndTagsData.contentTags.value - Value of applied Tag | ||
* @param {string} taxonomyAndTagsData.contentTags.lineage - Array of Tag's ancestors sorted (ancestor -> tag) | ||
*/ | ||
const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => { | ||
const intl = useIntl(); | ||
const { | ||
id, name, contentTags, | ||
} = taxonomyAndTagsData; | ||
|
||
const [isOpen, open, close] = useToggle(false); | ||
const [target, setTarget] = React.useState(null); | ||
|
||
return ( | ||
<div className="d-flex"> | ||
<Collapsible title={name} styling="card-lg" className="taxonomy-tags-collapsible"> | ||
<div key={id}> | ||
<ContentTagsTree appliedContentTags={contentTags} /> | ||
</div> | ||
|
||
<div className="d-flex taxonomy-tags-selector-menu"> | ||
<Button | ||
ref={setTarget} | ||
variant="outline-primary" | ||
onClick={open} | ||
> | ||
<FormattedMessage {...messages.addTagsButtonText} /> | ||
</Button> | ||
</div> | ||
<ModalPopup | ||
hasArrow | ||
placement="bottom" | ||
positionRef={target} | ||
isOpen={isOpen} | ||
onClose={close} | ||
> | ||
<div className="bg-white p-3 shadow"> | ||
|
||
<SelectableBox.Set | ||
type="checkbox" | ||
name="tags" | ||
columns={1} | ||
ariaLabel={intl.formatMessage(messages.taxonomyTagsAriaLabel)} | ||
className="taxonomy-tags-selectable-box-set" | ||
> | ||
<ContentTagsDropDownSelector | ||
key={`selector-${id}`} | ||
taxonomyId={id} | ||
level={0} | ||
/> | ||
</SelectableBox.Set> | ||
</div> | ||
</ModalPopup> | ||
|
||
</Collapsible> | ||
<div className="d-flex"> | ||
<Badge | ||
variant="light" | ||
pill | ||
className={classNames('align-self-start', 'mt-3', { | ||
// eslint-disable-next-line quote-props | ||
'invisible': contentTags.length === 0, | ||
})} | ||
> | ||
{contentTags.length} | ||
</Badge> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
ContentTagsCollapsible.propTypes = { | ||
taxonomyAndTagsData: PropTypes.shape({ | ||
id: PropTypes.number, | ||
name: PropTypes.string, | ||
contentTags: PropTypes.arrayOf(PropTypes.shape({ | ||
value: PropTypes.string, | ||
lineage: PropTypes.arrayOf(PropTypes.string), | ||
})), | ||
}).isRequired, | ||
}; | ||
|
||
export default ContentTagsCollapsible; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
.taxonomy-tags-collapsible { | ||
flex: 1; | ||
border: none !important; | ||
|
||
.collapsible-trigger { | ||
border: none !important; | ||
} | ||
} | ||
|
||
.taxonomy-tags-selector-menu { | ||
button { | ||
flex: 1; | ||
} | ||
} | ||
|
||
.taxonomy-tags-selector-menu + div { | ||
width: 100%; | ||
} | ||
|
||
.taxonomy-tags-selectable-box-set { | ||
grid-auto-rows: unset !important; | ||
overflow-y: scroll; | ||
max-height: 20rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import React from 'react'; | ||
import { IntlProvider } from '@edx/frontend-platform/i18n'; | ||
import { act, render } from '@testing-library/react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import ContentTagsCollapsible from './ContentTagsCollapsible'; | ||
|
||
jest.mock('./data/apiHooks', () => ({ | ||
useTaxonomyTagsDataResponse: jest.fn(), | ||
useIsTaxonomyTagsDataLoaded: jest.fn(), | ||
})); | ||
|
||
const data = { | ||
id: 123, | ||
name: 'Taxonomy 1', | ||
contentTags: [ | ||
{ | ||
value: 'Tag 1', | ||
lineage: ['Tag 1'], | ||
}, | ||
{ | ||
value: 'Tag 2', | ||
lineage: ['Tag 2'], | ||
}, | ||
], | ||
}; | ||
|
||
const ContentTagsCollapsibleComponent = ({ taxonomyAndTagsData }) => ( | ||
<IntlProvider locale="en" messages={{}}> | ||
<ContentTagsCollapsible taxonomyAndTagsData={taxonomyAndTagsData} /> | ||
</IntlProvider> | ||
); | ||
|
||
ContentTagsCollapsibleComponent.propTypes = { | ||
taxonomyAndTagsData: PropTypes.shape({ | ||
id: PropTypes.number, | ||
name: PropTypes.string, | ||
contentTags: PropTypes.arrayOf(PropTypes.shape({ | ||
value: PropTypes.string, | ||
lineage: PropTypes.arrayOf(PropTypes.string), | ||
})), | ||
}).isRequired, | ||
}; | ||
|
||
describe('<ContentTagsCollapsible />', () => { | ||
it('should render taxonomy tags data along content tags number badge', async () => { | ||
await act(async () => { | ||
const { container, getByText } = render(<ContentTagsCollapsibleComponent taxonomyAndTagsData={data} />); | ||
expect(getByText('Taxonomy 1')).toBeInTheDocument(); | ||
expect(container.getElementsByClassName('badge').length).toBe(1); | ||
expect(getByText('2')).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it('should render taxonomy tags data without tags number badge', async () => { | ||
data.contentTags = []; | ||
await act(async () => { | ||
const { container, getByText } = render(<ContentTagsCollapsibleComponent taxonomyAndTagsData={data} />); | ||
expect(getByText('Taxonomy 1')).toBeInTheDocument(); | ||
expect(container.getElementsByClassName('invisible').length).toBe(1); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React, { useMemo, useEffect } from 'react'; | ||
import { | ||
Container, | ||
CloseButton, | ||
Spinner, | ||
} from '@edx/paragon'; | ||
import { useIntl } from '@edx/frontend-platform/i18n'; | ||
import { useParams } from 'react-router-dom'; | ||
import messages from './messages'; | ||
import ContentTagsCollapsible from './ContentTagsCollapsible'; | ||
import { extractOrgFromContentId } from './utils'; | ||
import { | ||
useContentTaxonomyTagsDataResponse, | ||
useIsContentTaxonomyTagsDataLoaded, | ||
useContentDataResponse, | ||
useIsContentDataLoaded, | ||
} from './data/apiHooks'; | ||
import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/data/apiHooks'; | ||
import Loading from '../generic/Loading'; | ||
|
||
const ContentTagsDrawer = () => { | ||
const intl = useIntl(); | ||
const { contentId } = useParams(); | ||
|
||
const org = extractOrgFromContentId(contentId); | ||
|
||
const useContentData = () => { | ||
const contentData = useContentDataResponse(contentId); | ||
const isContentDataLoaded = useIsContentDataLoaded(contentId); | ||
return { contentData, isContentDataLoaded }; | ||
}; | ||
|
||
const useContentTaxonomyTagsData = () => { | ||
const contentTaxonomyTagsData = useContentTaxonomyTagsDataResponse(contentId); | ||
const isContentTaxonomyTagsLoaded = useIsContentTaxonomyTagsDataLoaded(contentId); | ||
return { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded }; | ||
}; | ||
|
||
const useTaxonomyListData = () => { | ||
const taxonomyListData = useTaxonomyListDataResponse(org); | ||
const isTaxonomyListLoaded = useIsTaxonomyListDataLoaded(org); | ||
return { taxonomyListData, isTaxonomyListLoaded }; | ||
}; | ||
|
||
const { contentData, isContentDataLoaded } = useContentData(); | ||
const { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded } = useContentTaxonomyTagsData(); | ||
const { taxonomyListData, isTaxonomyListLoaded } = useTaxonomyListData(); | ||
|
||
const closeContentTagsDrawer = () => { | ||
// "*" allows communication with any origin | ||
window.parent.postMessage('closeManageTagsDrawer', '*'); | ||
}; | ||
|
||
useEffect(() => { | ||
const handleEsc = (event) => { | ||
/* Close drawer when ESC-key is pressed and selectable dropdown box not open */ | ||
const selectableBoxOpen = document.querySelector('[data-selectable-box="taxonomy-tags"]'); | ||
if (event.key === 'Escape' && !selectableBoxOpen) { | ||
closeContentTagsDrawer(); | ||
} | ||
}; | ||
document.addEventListener('keydown', handleEsc); | ||
|
||
return () => { | ||
document.removeEventListener('keydown', handleEsc); | ||
}; | ||
}, []); | ||
|
||
const taxonomies = useMemo(() => { | ||
if (taxonomyListData && contentTaxonomyTagsData) { | ||
// Initialize list of content tags in taxonomies to populate | ||
const taxonomiesList = taxonomyListData.results.map((taxonomy) => { | ||
// eslint-disable-next-line no-param-reassign | ||
taxonomy.contentTags = []; | ||
return taxonomy; | ||
}); | ||
|
||
const contentTaxonomies = contentTaxonomyTagsData.taxonomies; | ||
|
||
// eslint-disable-next-line array-callback-return | ||
contentTaxonomies.map((contentTaxonomyTags) => { | ||
const contentTaxonomy = taxonomiesList.find((taxonomy) => taxonomy.id === contentTaxonomyTags.taxonomyId); | ||
if (contentTaxonomy) { | ||
contentTaxonomy.contentTags = contentTaxonomyTags.tags; | ||
} | ||
}); | ||
|
||
return taxonomiesList; | ||
} | ||
return []; | ||
}, [taxonomyListData, contentTaxonomyTagsData]); | ||
|
||
return ( | ||
|
||
<div className="mt-1"> | ||
<Container size="xl"> | ||
<CloseButton onClick={() => closeContentTagsDrawer()} data-testid="drawer-close-button" /> | ||
<span>{intl.formatMessage(messages.headerSubtitle)}</span> | ||
{ isContentDataLoaded | ||
? <h3>{ contentData.displayName }</h3> | ||
: ( | ||
<div className="d-flex justify-content-center align-items-center flex-column"> | ||
<Spinner | ||
animation="border" | ||
size="xl" | ||
screenReaderText={intl.formatMessage(messages.loadingMessage)} | ||
/> | ||
</div> | ||
)} | ||
|
||
<hr /> | ||
|
||
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded | ||
? taxonomies.map((data) => ( | ||
<div key={`taxonomy-tags-collapsible-${data.id}`}> | ||
<ContentTagsCollapsible taxonomyAndTagsData={data} /> | ||
<hr /> | ||
</div> | ||
)) | ||
: <Loading />} | ||
|
||
</Container> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ContentTagsDrawer; |
Oops, something went wrong.