Skip to content

Commit

Permalink
feat: Add content tags tree state + editing
Browse files Browse the repository at this point in the history
This commit adds the add/remove functionality of content tags where the
state is stored and changes are updated in the backend through the API.
Changes are reflected in the UI automatically.

style: Make custom style overrides more specific
fix: ModalPopup appears cut off when top of screen
refactor: Extract non-UI logic to helper component
refactor: Rename mutation -> updater for clarity
chore: Review nits
style: Remove hover style for tag bubbles
feat: Include implicit tags in tags count badge
refactor: Nits from PR review
fix: Copy tree to avoid modifying originals
test: Add tests for new content tags editing
test: Update tests for latest code
fix: Fix typing issues with mutations
fix: Clear checkboxes when mutation error
feat: Add `editable` flag to ContentTagsDrawer
feat: Invalidate query refetch content tags count
refactor: Simplify react-query hooks + fix types
fix: Add missing `contentId` to queryKey
refactor: Move util functions outside component
fix: Remove arrow from tags dropdown modal
feat: "Load more" button to handle paginated tags
chore: fix stylelint issues
fix: Use Chip instead of Button for TagBubble
feat: Add API calls to backend when updating tags
feat: Add content tags tree state + editing
  • Loading branch information
yusuf-musleh committed Dec 8, 2023
1 parent 56ad86e commit ae22fb1
Show file tree
Hide file tree
Showing 22 changed files with 988 additions and 424 deletions.
115 changes: 99 additions & 16 deletions src/content-tags-drawer/ContentTagsCollapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,79 @@ import ContentTagsDropDownSelector from './ContentTagsDropDownSelector';

import ContentTagsTree from './ContentTagsTree';

import useContentTagsCollapsibleHelper from './ContentTagsCollapsibleHelper';

/**
* 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.
*
* This component also handles all the logic with selecting/deselecting tags and keeps track of the
* tags tree in the state. That is used to render the Tag bubbgles as well as the populating the
* state of the tags in the dropdown selectors.
*
* The `contentTags` that is passed are consolidated and converted to a tree structure. For example:
*
* FROM:
*
* [
* {
* "value": "DNA Sequencing",
* "lineage": [
* "Science and Research",
* "Genetics Subcategory",
* "DNA Sequencing"
* ]
* },
* {
* "value": "Virology",
* "lineage": [
* "Science and Research",
* "Molecular, Cellular, and Microbiology",
* "Virology"
* ]
* }
* ]
*
* TO:
*
* {
* "Science and Research": {
* explicit: false,
* children: {
* "Genetics Subcategory": {
* explicit: false,
* children: {
* "DNA Sequencing": {
* explicit: true,
* children: {}
* }
* }
* },
* "Molecular, Cellular, and Microbiology": {
* explicit: false,
* children: {
* "Virology": {
* explicit: true,
* children: {}
* }
* }
* }
* }
* }
* };
*
*
* It also keeps track of newly added tags as they are selected in the dropdown selectors.
* They are store in the same format above, and then merged to one tree that is used as the
* source of truth for both the tag bubble and the dropdowns. They keys are order alphabetically.
*
* In the dropdowns, the value of each SelectableBox is stored along with it's lineage and is URI encoded.
* Ths is so we are able to traverse and manipulate different parts of the tree leading to it.
* Here is an example of what the value of the "Virology" tag would be:
*
* "Science%20and%20Research,Molecular%2C%20Cellular%2C%20and%20Microbiology,Virology"
* @param {string} contentId - Id of the content object
* @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
Expand All @@ -35,36 +104,46 @@ import ContentTagsTree from './ContentTagsTree';
* @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)
* @param {boolean} editable - Whether the tags can be edited
*/
const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => {
const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) => {
const intl = useIntl();
const { id, name } = taxonomyAndTagsData;

const {
id, name, contentTags,
} = taxonomyAndTagsData;
tagChangeHandler, tagsTree, contentTagsCount, checkedTags,
} = useContentTagsCollapsibleHelper(contentId, taxonomyAndTagsData);

const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = React.useState(null);
const [addTagsButtonRef, setAddTagsButtonRef] = React.useState(null);

const handleSelectableBoxChange = React.useCallback((e) => {
tagChangeHandler(e.target.value, e.target.checked);
});

return (
<div className="d-flex">
<Collapsible title={name} styling="card-lg" className="taxonomy-tags-collapsible">
<div key={id}>
<ContentTagsTree appliedContentTags={contentTags} />
<ContentTagsTree tagsTree={tagsTree} removeTagHandler={tagChangeHandler} editable={editable} />
</div>

<div className="d-flex taxonomy-tags-selector-menu">
<Button
ref={setTarget}
variant="outline-primary"
onClick={open}
>
<FormattedMessage {...messages.addTagsButtonText} />
</Button>

{editable && (
<Button
ref={setAddTagsButtonRef}
variant="outline-primary"
onClick={open}
>
<FormattedMessage {...messages.addTagsButtonText} />
</Button>
)}
</div>
<ModalPopup
hasArrow
placement="bottom"
positionRef={target}
positionRef={addTagsButtonRef}
isOpen={isOpen}
onClose={close}
>
Expand All @@ -76,11 +155,14 @@ const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => {
columns={1}
ariaLabel={intl.formatMessage(messages.taxonomyTagsAriaLabel)}
className="taxonomy-tags-selectable-box-set"
onChange={handleSelectableBoxChange}
value={checkedTags}
>
<ContentTagsDropDownSelector
key={`selector-${id}`}
taxonomyId={id}
level={0}
tagsTree={tagsTree}
/>
</SelectableBox.Set>
</div>
Expand All @@ -92,18 +174,18 @@ const ContentTagsCollapsible = ({ taxonomyAndTagsData }) => {
variant="light"
pill
className={classNames('align-self-start', 'mt-3', {
// eslint-disable-next-line quote-props
'invisible': contentTags.length === 0,
invisible: contentTagsCount === 0,
})}
>
{contentTags.length}
{contentTagsCount}
</Badge>
</div>
</div>
);
};

ContentTagsCollapsible.propTypes = {
contentId: PropTypes.string.isRequired,
taxonomyAndTagsData: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
Expand All @@ -112,6 +194,7 @@ ContentTagsCollapsible.propTypes = {
lineage: PropTypes.arrayOf(PropTypes.string),
})),
}).isRequired,
editable: PropTypes.bool.isRequired,
};

export default ContentTagsCollapsible;
4 changes: 4 additions & 0 deletions src/content-tags-drawer/ContentTagsCollapsible.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
overflow-y: scroll;
max-height: 20rem;
}

.pgn__modal-popup__arrow {
visibility: hidden;
}
Loading

0 comments on commit ae22fb1

Please sign in to comment.