-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FC-0036] feat: Add ContentTagsDrawer widget #654
Merged
arbrandes
merged 1 commit into
openedx:master
from
open-craft:yusuf-musleh/taxonomy-tags-drawer-widget
Nov 20, 2023
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't there is any set standard for this, but a few other places this is used it's a string like: 'lmshtmlfragment.resize' and 'tools.toggleNotes'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making this change would require also making changes to the edx-platform. I think so it doesn't get blocked by another review cycle there, we can address it in a later PR.