[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.
This commit is contained in:
Chris Chávez
2024-05-09 11:04:22 -05:00
committed by GitHub
parent 55adcfe90d
commit e0fb41d8f5
7 changed files with 232 additions and 3 deletions

View File

@@ -51,6 +51,7 @@ const ContentTagsDrawer = ({ id, onClose }) => {
toastMessage,
closeToast,
setCollapsibleToInitalState,
otherTaxonomies,
} = context;
let onCloseDrawer = onClose;
@@ -122,6 +123,29 @@ const ContentTagsDrawer = ({ id, onClose }) => {
</div>
))
: <Loading />}
{otherTaxonomies.length !== 0 && (
<div>
<p className="h4 text-gray-500 font-weight-bold">
{intl.formatMessage(messages.otherTagsHeader)}
</p>
<p className="other-description text-gray-500">
{intl.formatMessage(messages.otherTagsDescription)}
</p>
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
otherTaxonomies.map((data) => (
<div key={`taxonomy-tags-collapsible-${data.id}`}>
<ContentTagsCollapsible
contentId={contentId}
taxonomyAndTagsData={data}
stagedContentTags={stagedContentTags[data.id] || []}
collapsibleState={collapsibleStates[data.id] || false}
/>
<hr />
</div>
))
)}
</div>
)}
</Container>
</Container>

View File

@@ -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

View File

@@ -162,6 +162,100 @@ describe('<ContentTagsDrawer />', () => {
});
};
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('<ContentTagsDrawer />', () => {
expect(taxonomies[i].textContent).toBe(expectedOrder[i]);
}
});
it('should not show "Other tags" section', async () => {
setupMockDataForStagedTagsTesting();
render(<RootWrapper />);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.queryByText('Other tags')).not.toBeInTheDocument();
});
it('should show "Other tags" section', async () => {
setupMockDataWithOtherTagsTestings();
render(<RootWrapper />);
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(<RootWrapper />);
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();
});
});

View File

@@ -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,
};
};

View File

@@ -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

View File

@@ -14,6 +14,7 @@
* @property {number} taxonomyId
* @property {boolean} canTagObject
* @property {Tag[]} tags
* @property {string} exportId
*/
/**

View File

@@ -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;