From e0fb41d8f595bb1a0b5e8ba5365bce984b22067c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Thu, 9 May 2024 11:04:22 -0500 Subject: [PATCH] [FC-0049] feat: Other Tags section added on tags drawer (#987) Adds the new "Other tags" Section to tags drawer that contains the taxonomies/tags that the user doesn't have permission to see/edit. It allow to delete those tags. --- src/content-tags-drawer/ContentTagsDrawer.jsx | 24 +++ .../ContentTagsDrawer.scss | 4 + .../ContentTagsDrawer.test.jsx | 142 ++++++++++++++++++ .../ContentTagsDrawerHelper.jsx | 53 ++++++- src/content-tags-drawer/common/context.js | 1 + src/content-tags-drawer/data/types.mjs | 1 + src/content-tags-drawer/messages.js | 10 ++ 7 files changed, 232 insertions(+), 3 deletions(-) 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;