Skip to content

Commit

Permalink
Use object permissions in Tagging frontend [FC-0036] (openedx#787)
Browse files Browse the repository at this point in the history
Uses the permissions added to the Tagging REST API by openedx/openedx-learning#138 to decide what actions (e.g. import, export, edit, delete) to present to the current user when viewing Tagging-related content.
  • Loading branch information
pomegranited authored Jan 29, 2024
1 parent 90bc242 commit b56b5d9
Show file tree
Hide file tree
Showing 27 changed files with 308 additions and 197 deletions.
11 changes: 5 additions & 6 deletions src/content-tags-drawer/ContentTagsCollapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ import useContentTagsCollapsibleHelper from './ContentTagsCollapsibleHelper';
* @param {Object} props - The component props.
* @param {string} props.contentId - Id of the content object
* @param {TaxonomyData & {contentTags: ContentTagData[]}} props.taxonomyAndTagsData - Taxonomy metadata & applied tags
* @param {boolean} props.editable - Whether the tags can be edited
*/
const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) => {
const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData }) => {
const intl = useIntl();
const { id, name } = taxonomyAndTagsData;
const { id, name, canTagObject } = taxonomyAndTagsData;

const {
tagChangeHandler, tagsTree, contentTagsCount, checkedTags,
Expand Down Expand Up @@ -141,12 +140,12 @@ const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) =>
<div className="d-flex">
<Collapsible title={name} styling="card-lg" className="taxonomy-tags-collapsible">
<div key={id}>
<ContentTagsTree tagsTree={tagsTree} removeTagHandler={tagChangeHandler} editable={editable} />
<ContentTagsTree tagsTree={tagsTree} removeTagHandler={tagChangeHandler} />
</div>

<div className="d-flex taxonomy-tags-selector-menu">

{editable && (
{canTagObject && (
<Button
ref={setAddTagsButtonRef}
variant="outline-primary"
Expand Down Expand Up @@ -216,8 +215,8 @@ ContentTagsCollapsible.propTypes = {
value: PropTypes.string,
lineage: PropTypes.arrayOf(PropTypes.string),
})),
canTagObject: PropTypes.bool.isRequired,
}).isRequired,
editable: PropTypes.bool.isRequired,
};

export default ContentTagsCollapsible;
32 changes: 15 additions & 17 deletions src/content-tags-drawer/ContentTagsCollapsible.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
fireEvent,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PropTypes from 'prop-types';

import ContentTagsCollapsible from './ContentTagsCollapsible';
import messages from './messages';
Expand All @@ -22,6 +21,7 @@ jest.mock('./data/apiHooks', () => ({
tagPages: [{
isLoading: true,
isError: false,
canAddTag: false,
data: [],
}],
})),
Expand All @@ -32,42 +32,34 @@ const data = {
taxonomyAndTagsData: {
id: 123,
name: 'Taxonomy 1',
canTagObject: true,
contentTags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 1.1',
lineage: ['Tag 1', 'Tag 1.1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
editable: true,
};

const ContentTagsCollapsibleComponent = ({ contentId, taxonomyAndTagsData, editable }) => (
const ContentTagsCollapsibleComponent = ({ contentId, taxonomyAndTagsData }) => (
<IntlProvider locale="en" messages={{}}>
<ContentTagsCollapsible contentId={contentId} taxonomyAndTagsData={taxonomyAndTagsData} editable={editable} />
<ContentTagsCollapsible contentId={contentId} taxonomyAndTagsData={taxonomyAndTagsData} />
</IntlProvider>
);

ContentTagsCollapsibleComponent.propTypes = {
contentId: PropTypes.string.isRequired,
taxonomyAndTagsData: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
contentTags: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
lineage: PropTypes.arrayOf(PropTypes.string),
})),
}).isRequired,
editable: PropTypes.bool.isRequired,
};
ContentTagsCollapsibleComponent.propTypes = ContentTagsCollapsible.propTypes;

describe('<ContentTagsCollapsible />', () => {
beforeAll(() => {
Expand All @@ -85,14 +77,14 @@ describe('<ContentTagsCollapsible />', () => {
<ContentTagsCollapsibleComponent
contentId={componentData.contentId}
taxonomyAndTagsData={componentData.taxonomyAndTagsData}
editable={componentData.editable}
/>,
);
}

function setupTaxonomyMock() {
useTaxonomyTagsData.mockReturnValue({
hasMorePages: false,
canAddTag: false,
tagPages: [{
isLoading: false,
isError: false,
Expand All @@ -104,6 +96,8 @@ describe('<ContentTagsCollapsible />', () => {
parentValue: null,
id: 12345,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 2',
externalId: null,
Expand All @@ -112,6 +106,8 @@ describe('<ContentTagsCollapsible />', () => {
parentValue: null,
id: 12346,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}, {
value: 'Tag 3',
externalId: null,
Expand All @@ -120,6 +116,8 @@ describe('<ContentTagsCollapsible />', () => {
parentValue: null,
id: 12347,
subTagsUrl: null,
canChangeTag: false,
canDeleteTag: false,
}],
}],
});
Expand Down
13 changes: 10 additions & 3 deletions src/content-tags-drawer/ContentTagsCollapsibleHelper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const removeTags = (tree, tagsToRemove) => {
*/
const useContentTagsCollapsibleHelper = (contentId, taxonomyAndTagsData) => {
const {
id, contentTags,
id, contentTags, canTagObject,
} = taxonomyAndTagsData;
// State to determine whether the tags are being updating so we can make a call
// to the update endpoint to the reflect those changes
Expand All @@ -101,7 +101,7 @@ const useContentTagsCollapsibleHelper = (contentId, taxonomyAndTagsData) => {
const tags = checkedTags.map(t => decodeURIComponent(t.split(',').slice(-1)));
updateTags.mutate({ tags });
}
}, [contentId, id, checkedTags]);
}, [contentId, id, canTagObject, checkedTags]);

// This converts the contentTags prop to the tree structure mentioned above
const appliedContentTags = React.useMemo(() => {
Expand All @@ -128,6 +128,8 @@ const useContentTagsCollapsibleHelper = (contentId, taxonomyAndTagsData) => {
currentLevel[key] = {
explicit: isExplicit,
children: {},
canChangeObjecttag: item.canChangeObjecttag,
canDeleteObjecttag: item.canDeleteObjecttag,
};

// Populating the SelectableBox with "selected" (explicit) tags
Expand Down Expand Up @@ -162,7 +164,12 @@ const useContentTagsCollapsibleHelper = (contentId, taxonomyAndTagsData) => {
const isExplicit = selectedTag === tag;

if (!traversal[tag]) {
traversal[tag] = { explicit: isExplicit, children: {} };
traversal[tag] = {
explicit: isExplicit,
children: {},
canChangeObjecttag: false,
canDeleteObjecttag: false,
};
} else {
traversal[tag].explicit = isExplicit;
}
Expand Down
3 changes: 1 addition & 2 deletions src/content-tags-drawer/ContentTagsDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ const ContentTagsDrawer = () => {
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded
? taxonomies.map((data) => (
<div key={`taxonomy-tags-collapsible-${data.id}`}>
{/* TODO: Properly set whether tags should be editable or not based on permissions */}
<ContentTagsCollapsible contentId={contentId} taxonomyAndTagsData={data} editable />
<ContentTagsCollapsible contentId={contentId} taxonomyAndTagsData={data} />
<hr />
</div>
))
Expand Down
9 changes: 7 additions & 2 deletions src/content-tags-drawer/ContentTagsDrawer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,29 @@ describe('<ContentTagsDrawer />', () => {
{
name: 'Taxonomy 1',
taxonomyId: 123,
editable: true,
canTagObject: true,
tags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
canDeleteObjecttag: true,
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
canDeleteObjecttag: true,
},
],
},
{
name: 'Taxonomy 2',
taxonomyId: 124,
editable: true,
canTagObject: true,
tags: [
{
value: 'Tag 3',
lineage: ['Tag 3'],
canDeleteObjecttag: true,
},
],
},
Expand All @@ -117,10 +120,12 @@ describe('<ContentTagsDrawer />', () => {
id: 123,
name: 'Taxonomy 1',
description: 'This is a description 1',
canTagObject: false,
}, {
id: 124,
name: 'Taxonomy 2',
description: 'This is a description 2',
canTagObject: false,
}],
});
await act(async () => {
Expand Down
14 changes: 1 addition & 13 deletions src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
waitFor,
fireEvent,
} from '@testing-library/react';
import PropTypes from 'prop-types';

import ContentTagsDropDownSelector from './ContentTagsDropDownSelector';
import { useTaxonomyTagsData } from './data/apiHooks';
Expand Down Expand Up @@ -47,18 +46,7 @@ ContentTagsDropDownSelectorComponent.defaultProps = {
searchTerm: '',
};

ContentTagsDropDownSelectorComponent.propTypes = {
taxonomyId: PropTypes.number.isRequired,
level: PropTypes.number.isRequired,
lineage: PropTypes.arrayOf(PropTypes.string),
tagsTree: PropTypes.objectOf(
PropTypes.shape({
explicit: PropTypes.bool.isRequired,
children: PropTypes.shape({}).isRequired,
}).isRequired,
).isRequired,
searchTerm: PropTypes.string,
};
ContentTagsDropDownSelectorComponent.propTypes = ContentTagsDropDownSelector.propTypes;

describe('<ContentTagsDropDownSelector />', () => {
afterEach(() => {
Expand Down
7 changes: 3 additions & 4 deletions src/content-tags-drawer/ContentTagsTree.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,8 @@ import TagBubble from './TagBubble';
* tagSelectableBoxValue: string,
* checked: boolean
* ) => void} props.removeTagHandler - Function that is called when removing tags from the tree.
* @param {boolean} props.editable - Whether the tags appear with an 'x' allowing the user to remove them.
*/
const ContentTagsTree = ({ tagsTree, removeTagHandler, editable }) => {
const ContentTagsTree = ({ tagsTree, removeTagHandler }) => {
const renderTagsTree = (tag, level, lineage) => Object.keys(tag).map((key) => {
const updatedLineage = [...lineage, encodeURIComponent(key)];
if (tag[key] !== undefined) {
Expand All @@ -56,7 +55,7 @@ const ContentTagsTree = ({ tagsTree, removeTagHandler, editable }) => {
level={level}
lineage={updatedLineage}
removeTagHandler={removeTagHandler}
editable={editable}
canRemove={tag[key].canDeleteObjecttag}
/>
{ renderTagsTree(tag[key].children, level + 1, updatedLineage) }
</div>
Expand All @@ -73,10 +72,10 @@ ContentTagsTree.propTypes = {
PropTypes.shape({
explicit: PropTypes.bool.isRequired,
children: PropTypes.shape({}).isRequired,
canDeleteObjecttag: PropTypes.bool.isRequired,
}).isRequired,
).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
};

export default ContentTagsTree;
23 changes: 9 additions & 14 deletions src/content-tags-drawer/ContentTagsTree.test.jsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,52 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { act, render } from '@testing-library/react';
import PropTypes from 'prop-types';

import ContentTagsTree from './ContentTagsTree';

const data = {
'Science and Research': {
explicit: false,
canDeleteObjecttag: false,
children: {
'Genetics Subcategory': {
explicit: false,
children: {
'DNA Sequencing': {
explicit: true,
children: {},
canDeleteObjecttag: true,
},
},
canDeleteObjecttag: false,
},
'Molecular, Cellular, and Microbiology': {
explicit: false,
children: {
Virology: {
explicit: true,
children: {},
canDeleteObjecttag: true,
},
},
canDeleteObjecttag: false,
},
},
},
};

const ContentTagsTreeComponent = ({ tagsTree, removeTagHandler, editable }) => (
const ContentTagsTreeComponent = ({ tagsTree, removeTagHandler }) => (
<IntlProvider locale="en" messages={{}}>
<ContentTagsTree tagsTree={tagsTree} removeTagHandler={removeTagHandler} editable={editable} />
<ContentTagsTree tagsTree={tagsTree} removeTagHandler={removeTagHandler} />
</IntlProvider>
);

ContentTagsTreeComponent.propTypes = {
tagsTree: PropTypes.objectOf(
PropTypes.shape({
explicit: PropTypes.bool.isRequired,
children: PropTypes.shape({}).isRequired,
}).isRequired,
).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
};
ContentTagsTreeComponent.propTypes = ContentTagsTree.propTypes;

describe('<ContentTagsTree />', () => {
it('should render taxonomy tags data along content tags number badge', async () => {
await act(async () => {
const { getByText } = render(<ContentTagsTreeComponent tagsTree={data} removeTagHandler={() => {}} editable />);
const { getByText } = render(<ContentTagsTreeComponent tagsTree={data} removeTagHandler={() => {}} />);
expect(getByText('Science and Research')).toBeInTheDocument();
expect(getByText('Genetics Subcategory')).toBeInTheDocument();
expect(getByText('Molecular, Cellular, and Microbiology')).toBeInTheDocument();
Expand Down
Loading

0 comments on commit b56b5d9

Please sign in to comment.