Skip to content

Commit

Permalink
feat: Content Tags Drawer uses permissions returned by the content ta…
Browse files Browse the repository at this point in the history
…gging REST API

to decide what actions to offer the user.

These object-specific permissions replace the global "editable" field,
which has been removed from the REST API.
  • Loading branch information
pomegranited committed Jan 11, 2024
1 parent f6a3487 commit 93b0ad0
Show file tree
Hide file tree
Showing 16 changed files with 49 additions and 47 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;
9 changes: 4 additions & 5 deletions src/content-tags-drawer/ContentTagsCollapsible.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ const data = {
lineage: ['Tag 2'],
},
],
canTagObject: 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>
);

Expand All @@ -65,8 +65,8 @@ ContentTagsCollapsibleComponent.propTypes = {
value: PropTypes.string,
lineage: PropTypes.arrayOf(PropTypes.string),
})),
canTagObjects: PropTypes.bool.isRequired,
}).isRequired,
editable: PropTypes.bool.isRequired,
};

describe('<ContentTagsCollapsible />', () => {
Expand All @@ -85,7 +85,6 @@ describe('<ContentTagsCollapsible />', () => {
<ContentTagsCollapsibleComponent
contentId={componentData.contentId}
taxonomyAndTagsData={componentData.taxonomyAndTagsData}
editable={componentData.editable}
/>,
);
}
Expand Down
5 changes: 3 additions & 2 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,7 @@ const useContentTagsCollapsibleHelper = (contentId, taxonomyAndTagsData) => {
currentLevel[key] = {
explicit: isExplicit,
children: {},
canDelete: item.canDelete,
};

// Populating the SelectableBox with "selected" (explicit) tags
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
4 changes: 2 additions & 2 deletions src/content-tags-drawer/ContentTagsDrawer.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('<ContentTagsDrawer />', () => {
{
name: 'Taxonomy 1',
taxonomyId: 123,
editable: true,
canTagObject: true,
tags: [
{
value: 'Tag 1',
Expand All @@ -101,7 +101,7 @@ describe('<ContentTagsDrawer />', () => {
{
name: 'Taxonomy 2',
taxonomyId: 124,
editable: true,
canTagObject: true,
tags: [
{
value: 'Tag 3',
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}
canDelete={tag[key].canDelete}
/>
{ 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,
canDelete: PropTypes.bool.isRequired,
}).isRequired,
).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
};

export default ContentTagsTree;
12 changes: 8 additions & 4 deletions src/content-tags-drawer/ContentTagsTree.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,29 @@ const data = {
'DNA Sequencing': {
explicit: true,
children: {},
canDelete: true,
},
},
canDelete: false,
},
'Molecular, Cellular, and Microbiology': {
explicit: false,
children: {
Virology: {
explicit: true,
children: {},
canDelete: true,
},
},
canDelete: 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>
);

Expand All @@ -42,16 +46,16 @@ ContentTagsTreeComponent.propTypes = {
PropTypes.shape({
explicit: PropTypes.bool.isRequired,
children: PropTypes.shape({}).isRequired,
canDelete: PropTypes.bool.isRequired,
}).isRequired,
).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
};

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
10 changes: 5 additions & 5 deletions src/content-tags-drawer/TagBubble.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ import PropTypes from 'prop-types';
import TagOutlineIcon from './TagOutlineIcon';

const TagBubble = ({
value, implicit, level, lineage, removeTagHandler, editable,
value, implicit, level, lineage, removeTagHandler, canDelete,
}) => {
const className = `tag-bubble mb-2 border-light-300 ${implicit ? 'implicit' : ''}`;

const handleClick = React.useCallback(() => {
if (!implicit && editable) {
if (!implicit && canDelete) {
removeTagHandler(lineage.join(','), false);
}
}, [implicit, lineage, editable, removeTagHandler]);
}, [implicit, lineage, canDelete, removeTagHandler]);

return (
<div style={{ paddingLeft: `${level * 1}rem` }}>
<Chip
className={className}
variant="light"
iconBefore={!implicit ? Tag : TagOutlineIcon}
iconAfter={!implicit && editable ? Close : null}
iconAfter={!implicit && canDelete ? Close : null}
onIconAfterClick={handleClick}
>
{value}
Expand All @@ -44,7 +44,7 @@ TagBubble.propTypes = {
level: PropTypes.number,
lineage: PropTypes.arrayOf(PropTypes.string).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired,
};

export default TagBubble;
12 changes: 6 additions & 6 deletions src/content-tags-drawer/TagBubble.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const data = {
};

const TagBubbleComponent = ({
value, implicit, level, lineage, removeTagHandler, editable,
value, implicit, level, lineage, removeTagHandler, canDelete,
}) => (
<IntlProvider locale="en" messages={{}}>
<TagBubble
Expand All @@ -21,7 +21,7 @@ const TagBubbleComponent = ({
level={level}
lineage={lineage}
removeTagHandler={removeTagHandler}
editable={editable}
canDelete={canDelete}
/>
</IntlProvider>
);
Expand All @@ -37,15 +37,15 @@ TagBubbleComponent.propTypes = {
level: PropTypes.number,
lineage: PropTypes.arrayOf(PropTypes.string).isRequired,
removeTagHandler: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
canDelete: PropTypes.bool.isRequired,
};

describe('<TagBubble />', () => {
it('should render implicit tag', () => {
const { container, getByText } = render(
<TagBubbleComponent
value={data.value}
editable
canDelete
lineage={data.lineage}
removeTagHandler={data.removeTagHandler}
/>,
Expand All @@ -63,7 +63,7 @@ describe('<TagBubble />', () => {
const { container, getByText } = render(
<TagBubbleComponent
value={tagBubbleData.value}
editable
canDelete
lineage={data.lineage}
implicit={tagBubbleData.implicit}
removeTagHandler={tagBubbleData.removeTagHandler}
Expand All @@ -82,7 +82,7 @@ describe('<TagBubble />', () => {
const { container } = render(
<TagBubbleComponent
value={tagBubbleData.value}
editable
canDelete
lineage={data.lineage}
implicit={tagBubbleData.implicit}
removeTagHandler={tagBubbleData.removeTagHandler}
Expand Down
4 changes: 2 additions & 2 deletions src/content-tags-drawer/__mocks__/contentTaxonomyTagsMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
{
name: 'FlatTaxonomy',
taxonomyId: 3,
editable: true,
canTagObject: true,
tags: [
{
value: 'flat taxonomy tag 3856',
Expand All @@ -17,7 +17,7 @@ module.exports = {
{
name: 'HierarchicalTaxonomy',
taxonomyId: 4,
editable: true,
canTagObject: true,
tags: [
{
value: 'hierarchical taxonomy tag 1.7.59',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
{
name: 'FlatTaxonomy',
taxonomyId: 3,
editable: true,
canTagObject: true,
tags: [
{
value: 'flat taxonomy tag 100',
Expand Down
6 changes: 3 additions & 3 deletions src/content-tags-drawer/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
* @returns {string} the URL
*/
export const getTaxonomyTagsApiUrl = (taxonomyId, options = {}) => {
const url = new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl());
const url = new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/?include_perms`, getApiBaseUrl());
if (options.parentTag) {
url.searchParams.append('parent_tag', options.parentTag);
}
Expand All @@ -28,7 +28,7 @@ export const getTaxonomyTagsApiUrl = (taxonomyId, options = {}) => {

return url.href;
};
export const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href;
export const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/?include_perms`, getApiBaseUrl()).href;
export const getXBlockContentDataApiURL = (contentId) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href;
export const getLibraryContentDataApiUrl = (contentId) => new URL(`/api/libraries/v2/blocks/${contentId}/`, getApiBaseUrl()).href;

Expand Down Expand Up @@ -76,7 +76,7 @@ export async function getContentData(contentId) {
*/
export async function updateContentTaxonomyTags(contentId, taxonomyId, tags) {
let url = getContentTaxonomyTagsApiUrl(contentId);
url = `${url}?taxonomy=${taxonomyId}`;
url = `${url}&taxonomy=${taxonomyId}`;
const { data } = await getAuthenticatedHttpClient().put(url, { tags });
return camelCaseObject(data[contentId]);
}
4 changes: 2 additions & 2 deletions src/content-tags-drawer/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,10 @@ describe('content tags drawer api calls', () => {
const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb0b';
const taxonomyId = 3;
const tags = ['flat taxonomy tag 100', 'flat taxonomy tag 3856'];
axiosMock.onPut(`${getContentTaxonomyTagsApiUrl(contentId)}?taxonomy=${taxonomyId}`).reply(200, updateContentTaxonomyTagsMock);
axiosMock.onPut(`${getContentTaxonomyTagsApiUrl(contentId)}&taxonomy=${taxonomyId}`).reply(200, updateContentTaxonomyTagsMock);
const result = await updateContentTaxonomyTags(contentId, taxonomyId, tags);

expect(axiosMock.history.put[0].url).toEqual(`${getContentTaxonomyTagsApiUrl(contentId)}?taxonomy=${taxonomyId}`);
expect(axiosMock.history.put[0].url).toEqual(`${getContentTaxonomyTagsApiUrl(contentId)}&taxonomy=${taxonomyId}`);
expect(result).toEqual(updateContentTaxonomyTagsMock[contentId]);
});
});
2 changes: 1 addition & 1 deletion src/content-tags-drawer/data/types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @typedef {Object} ContentTaxonomyTagData A list of the tags from one taxonomy that are applied to a content object.
* @property {string} name
* @property {number} taxonomyId
* @property {boolean} editable
* @property {boolean} canTagObject
* @property {Tag[]} tags
*/

Expand Down
4 changes: 2 additions & 2 deletions src/taxonomy/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

export const getTaxonomyListApiUrl = (org) => {
const url = new URL('api/content_tagging/v1/taxonomies/', getApiBaseUrl());
const url = new URL('api/content_tagging/v1/taxonomies/?include_perms', getApiBaseUrl());
url.searchParams.append('enabled', 'true');
if (org !== undefined) {
if (org === 'Unassigned') {
Expand All @@ -32,7 +32,7 @@ export const getTaxonomyTemplateApiUrl = (format) => new URL(
* @param {number} pk
* @returns {string}
*/
export const getTaxonomyApiUrl = (pk) => new URL(`api/content_tagging/v1/taxonomies/${pk}/`, getApiBaseUrl()).href;
export const getTaxonomyApiUrl = (pk) => new URL(`api/content_tagging/v1/taxonomies/${pk}/?include_perms`, getApiBaseUrl()).href;

/**
* Get list of taxonomies.
Expand Down
Loading

0 comments on commit 93b0ad0

Please sign in to comment.