diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx
index d8a69079f..e5f2e47de 100644
--- a/src/content-tags-drawer/ContentTagsDrawer.jsx
+++ b/src/content-tags-drawer/ContentTagsDrawer.jsx
@@ -51,6 +51,7 @@ const ContentTagsDrawer = ({ id, onClose }) => {
toastMessage,
closeToast,
setCollapsibleToInitalState,
+ otherTaxonomies,
} = context;
let onCloseDrawer = onClose;
@@ -122,6 +123,29 @@ const ContentTagsDrawer = ({ id, onClose }) => {
))
: }
+ {otherTaxonomies.length !== 0 && (
+
+
+ {intl.formatMessage(messages.otherTagsHeader)}
+
+
+ {intl.formatMessage(messages.otherTagsDescription)}
+
+ { isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
+ otherTaxonomies.map((data) => (
+
+
+
+
+ ))
+ )}
+
+ )}
diff --git a/src/content-tags-drawer/ContentTagsDrawer.scss b/src/content-tags-drawer/ContentTagsDrawer.scss
index e71684089..6124ff933 100644
--- a/src/content-tags-drawer/ContentTagsDrawer.scss
+++ b/src/content-tags-drawer/ContentTagsDrawer.scss
@@ -18,6 +18,10 @@
background-color: transparent;
color: $gray-300 !important;
}
+
+ .other-description {
+ font-size: .9rem;
+ }
}
// Apply styles to sheet only if it has a child with a .tags-drawer class
diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx
index 19376ac67..b9891fde2 100644
--- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx
+++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx
@@ -162,6 +162,100 @@ describe('', () => {
});
};
+ const setupMockDataWithOtherTagsTestings = () => {
+ useContentTaxonomyTagsData.mockReturnValue({
+ isSuccess: true,
+ data: {
+ taxonomies: [
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 123,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: 'Taxonomy 2',
+ taxonomyId: 1234,
+ canTagObject: false,
+ tags: [
+ {
+ value: 'Tag 3',
+ lineage: ['Tag 3'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 4',
+ lineage: ['Tag 4'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ ],
+ },
+ });
+ getTaxonomyListData.mockResolvedValue({
+ results: [
+ {
+ id: 123,
+ name: 'Taxonomy 1',
+ description: 'This is a description 1',
+ canTagObject: true,
+ },
+ ],
+ });
+
+ useTaxonomyTagsData.mockReturnValue({
+ hasMorePages: false,
+ canAddTag: false,
+ tagPages: {
+ isLoading: false,
+ isError: false,
+ data: [{
+ value: 'Tag 1',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12345,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ }, {
+ value: 'Tag 2',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12346,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ }, {
+ value: 'Tag 3',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12347,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ }],
+ },
+ });
+ };
+
const setupLargeMockDataForStagedTagsTesting = () => {
useContentTaxonomyTagsData.mockReturnValue({
isSuccess: true,
@@ -915,4 +1009,52 @@ describe('', () => {
expect(taxonomies[i].textContent).toBe(expectedOrder[i]);
}
});
+
+ it('should not show "Other tags" section', async () => {
+ setupMockDataForStagedTagsTesting();
+
+ render();
+ expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
+
+ expect(screen.queryByText('Other tags')).not.toBeInTheDocument();
+ });
+
+ it('should show "Other tags" section', async () => {
+ setupMockDataWithOtherTagsTestings();
+
+ render();
+ expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
+
+ expect(screen.getByText('Other tags')).toBeInTheDocument();
+ expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
+ expect(screen.getByText('Tag 3')).toBeInTheDocument();
+ expect(screen.getByText('Tag 4')).toBeInTheDocument();
+ });
+
+ it('should test delete "Other tags" and cancel', async () => {
+ setupMockDataWithOtherTagsTestings();
+ render();
+ expect(await screen.findByText('Taxonomy 2')).toBeInTheDocument();
+
+ // To edit mode
+ const editTagsButton = screen.getByRole('button', {
+ name: /edit tags/i,
+ });
+ fireEvent.click(editTagsButton);
+
+ // Delete the tag
+ const tag = screen.getByText(/tag 3/i);
+ const deleteButton = within(tag).getByRole('button', {
+ name: /delete/i,
+ });
+ fireEvent.click(deleteButton);
+
+ expect(tag).not.toBeInTheDocument();
+
+ // Click "Cancel"
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
+ fireEvent.click(cancelButton);
+
+ expect(screen.getByText(/tag 3/i)).toBeInTheDocument();
+ });
});
diff --git a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx
index b74f9db4e..8eb3e0e57 100644
--- a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx
+++ b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx
@@ -43,6 +43,7 @@ import { ContentTagsDrawerSheetContext } from './common/context';
* showToastAfterSave: () => void,
* closeToast: () => void,
* setCollapsibleToInitalState: () => void,
+ * otherTaxonomies: TagsInTaxonomy[],
* }}
*/
const useContentTagsDrawerContext = (contentId) => {
@@ -61,6 +62,8 @@ const useContentTagsDrawerContext = (contentId) => {
const [globalStagedRemovedContentTags, setGlobalStagedRemovedContentTags] = React.useState({});
// Merges feched tags, global staged tags and global removed staged tags
const [tagsByTaxonomy, setTagsByTaxonomy] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
+ // Other taxonomies that the user doesn't have permissions
+ const [otherTaxonomies, setOtherTaxonomies] = React.useState(/** @type TagsInTaxonomy[] */ ([]));
// This stores taxonomy collapsible states (open/close).
const [collapsibleStates, setColapsibleStates] = React.useState({});
// Message to show a toast in the content drawer.
@@ -77,7 +80,7 @@ const useContentTagsDrawerContext = (contentId) => {
const { data: taxonomyListData, isSuccess: isTaxonomyListLoaded } = useTaxonomyList(org);
// Tags feched from database
- const fechedTaxonomies = React.useMemo(() => {
+ const { fechedTaxonomies, fechedOtherTaxonomies } = React.useMemo(() => {
const sortTaxonomies = (taxonomiesList) => {
const taxonomiesWithData = taxonomiesList.filter(
(t) => t.contentTags.length !== 0,
@@ -117,17 +120,37 @@ const useContentTagsDrawerContext = (contentId) => {
const contentTaxonomies = contentTaxonomyTagsData.taxonomies;
+ const otherTaxonomiesList = [];
+
// 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;
+ } else {
+ otherTaxonomiesList.push({
+ canChangeTaxonomy: false,
+ canDeleteTaxonomy: false,
+ canTagObject: false,
+ contentTags: contentTaxonomyTags.tags,
+ enabled: true,
+ exportId: contentTaxonomyTags.exportId,
+ id: contentTaxonomyTags.taxonomyId,
+ name: contentTaxonomyTags.name,
+ visibleToAuthors: true,
+ });
}
});
- return sortTaxonomies(taxonomiesList);
+ return {
+ fechedTaxonomies: sortTaxonomies(taxonomiesList),
+ fechedOtherTaxonomies: otherTaxonomiesList,
+ };
}
- return [];
+ return {
+ fechedTaxonomies: [],
+ fechedOtherTaxonomies: [],
+ };
}, [taxonomyListData, contentTaxonomyTagsData]);
// Add a content tags to the staged tags for a taxonomy
@@ -204,6 +227,9 @@ const useContentTagsDrawerContext = (contentId) => {
fechedTaxonomies.forEach((taxonomy) => {
updatedState[taxonomy.id] = true;
});
+ fechedOtherTaxonomies.forEach((taxonomy) => {
+ updatedState[taxonomy.id] = true;
+ });
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
@@ -214,6 +240,10 @@ const useContentTagsDrawerContext = (contentId) => {
// Taxonomy with content tags must be open
updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
});
+ fechedOtherTaxonomies.forEach((taxonomy) => {
+ // Taxonomy with content tags must be open
+ updatedState[taxonomy.id] = taxonomy.contentTags.length !== 0;
+ });
setColapsibleStates(updatedState);
}, [fechedTaxonomies, setColapsibleStates]);
@@ -310,6 +340,10 @@ const useContentTagsDrawerContext = (contentId) => {
{ ...acc, [obj.id]: obj }
), {});
+ const mergedOtherTaxonomies = cloneDeep(fechedOtherTaxonomies).reduce((acc, obj) => (
+ { ...acc, [obj.id]: obj }
+ ), {});
+
Object.keys(globalStagedContentTags).forEach((taxonomyId) => {
if (mergedTags[taxonomyId]) {
// TODO test this
@@ -329,6 +363,10 @@ const useContentTagsDrawerContext = (contentId) => {
mergedTags[taxonomyId].contentTags = mergedTags[taxonomyId].contentTags.filter(
(t) => !globalStagedRemovedContentTags[taxonomyId].includes(t.value),
);
+ } else if (mergedOtherTaxonomies[taxonomyId]) {
+ mergedOtherTaxonomies[taxonomyId].contentTags = mergedOtherTaxonomies[taxonomyId].contentTags.filter(
+ (t) => !globalStagedRemovedContentTags[taxonomyId].includes(t.value),
+ );
}
});
@@ -337,6 +375,7 @@ const useContentTagsDrawerContext = (contentId) => {
const mergedTagsArray = fechedTaxonomies.map(obj => mergedTags[obj.id]);
setTagsByTaxonomy(mergedTagsArray);
+ setOtherTaxonomies(Object.values(mergedOtherTaxonomies));
if (setBlockingSheet) {
const areChangesInTags = () => {
@@ -364,6 +403,7 @@ const useContentTagsDrawerContext = (contentId) => {
}
}, [
fechedTaxonomies,
+ fechedOtherTaxonomies,
globalStagedContentTags,
globalStagedRemovedContentTags,
]);
@@ -376,6 +416,12 @@ const useContentTagsDrawerContext = (contentId) => {
tags: tags.contentTags.map(t => t.value),
});
});
+ otherTaxonomies.forEach((tags) => {
+ tagsData.push({
+ taxonomy: tags.id,
+ tags: tags.contentTags.map(t => t.value),
+ });
+ });
// @ts-ignore
updateTags.mutate({ tagsData });
}, [tagsByTaxonomy]);
@@ -408,6 +454,7 @@ const useContentTagsDrawerContext = (contentId) => {
showToastAfterSave,
closeToast,
setCollapsibleToInitalState,
+ otherTaxonomies,
};
};
diff --git a/src/content-tags-drawer/common/context.js b/src/content-tags-drawer/common/context.js
index 875823200..7aab97aa6 100644
--- a/src/content-tags-drawer/common/context.js
+++ b/src/content-tags-drawer/common/context.js
@@ -34,6 +34,7 @@ export const ContentTagsDrawerContext = React.createContext({
showToastAfterSave: /** @type{() => void} */ (() => {}),
closeToast: /** @type{() => void} */ (() => {}),
setCollapsibleToInitalState: /** @type{() => void} */ (() => {}),
+ otherTaxonomies: /** @type{TagsInTaxonomy[]} */ ([]),
});
// This context has not been added to ContentTagsDrawerContext because it has been
diff --git a/src/content-tags-drawer/data/types.mjs b/src/content-tags-drawer/data/types.mjs
index 5a4c1bf66..42de8e3a2 100644
--- a/src/content-tags-drawer/data/types.mjs
+++ b/src/content-tags-drawer/data/types.mjs
@@ -14,6 +14,7 @@
* @property {number} taxonomyId
* @property {boolean} canTagObject
* @property {Tag[]} tags
+ * @property {string} exportId
*/
/**
diff --git a/src/content-tags-drawer/messages.js b/src/content-tags-drawer/messages.js
index a316bcc54..ca9c7896c 100644
--- a/src/content-tags-drawer/messages.js
+++ b/src/content-tags-drawer/messages.js
@@ -114,6 +114,16 @@ const messages = defineMessages({
defaultMessage: 'Delete',
description: 'Alt label for Delete tag button.',
},
+ otherTagsHeader: {
+ id: 'course-authoring.content-tags-drawer.other-tags.header',
+ defaultMessage: 'Other tags',
+ description: 'Header of "Other tags" subsection in tags drawer',
+ },
+ otherTagsDescription: {
+ id: 'course-authoring.content-tags-drawer.other-tags.description',
+ defaultMessage: 'These tags are already applied, but you can\'t add new ones as you don\'t have access to their taxonomies.',
+ description: 'Description of "Other tags" subsection in tags drawer',
+ },
});
export default messages;