Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: example of pre-loading tags #18

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d020cd
feat: Add content tags tree state + editing
yusuf-musleh Nov 21, 2023
f40f1ea
feat: Add API calls to backend when updating tags
yusuf-musleh Nov 22, 2023
5929062
fix: Use Chip instead of Button for TagBubble
yusuf-musleh Nov 23, 2023
aac5c56
chore: fix stylelint issues
yusuf-musleh Nov 23, 2023
debe73b
feat: "Load more" button to handle paginated tags
yusuf-musleh Nov 23, 2023
c5165f2
fix: Remove arrow from tags dropdown modal
yusuf-musleh Nov 23, 2023
7c02136
refactor: Move util functions outside component
yusuf-musleh Nov 24, 2023
ec2c842
fix: Add missing `contentId` to queryKey
yusuf-musleh Nov 24, 2023
bdc8d30
refactor: Simplify react-query hooks + fix types
yusuf-musleh Nov 24, 2023
ae34667
feat: Invalidate query refetch content tags count
yusuf-musleh Nov 24, 2023
9592d11
feat: Add `editable` flag to ContentTagsDrawer
yusuf-musleh Nov 26, 2023
313728b
fix: Clear checkboxes when mutation error
yusuf-musleh Nov 26, 2023
9271338
fix: Fix typing issues with mutations
yusuf-musleh Nov 27, 2023
b9e9657
test: Update tests for latest code
yusuf-musleh Nov 27, 2023
63e965c
test: Add tests for new content tags editing
yusuf-musleh Nov 28, 2023
9fd8c6e
fix: Copy tree to avoid modifying originals
yusuf-musleh Nov 29, 2023
a5087dd
refactor: Nits from PR review
yusuf-musleh Nov 29, 2023
17bae44
feat: Include implicit tags in tags count badge
yusuf-musleh Nov 29, 2023
d8ac46a
style: Remove hover style for tag bubbles
yusuf-musleh Nov 29, 2023
21db528
feat: Add tag search in content tags drawer
yusuf-musleh Dec 1, 2023
e2c3987
style: Fix style/spacing inconsistencies
yusuf-musleh Dec 4, 2023
77fe712
fix: Spacing of tags in dropdown selector
yusuf-musleh Dec 5, 2023
4758625
WIP
yusuf-musleh Dec 5, 2023
0137be8
feat: example of pre-loading tags
bradenmacdonald Dec 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
330 changes: 315 additions & 15 deletions src/content-tags-drawer/ContentTagsCollapsible.jsx

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/content-tags-drawer/ContentTagsCollapsible.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

.taxonomy-tags-selectable-box-set {
grid-auto-rows: unset !important;
grid-gap: unset !important;
overflow-y: scroll;
max-height: 20rem;
}
197 changes: 176 additions & 21 deletions src/content-tags-drawer/ContentTagsCollapsible.test.jsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,59 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { act, render } from '@testing-library/react';
import {
act,
render,
fireEvent,
waitFor,
} from '@testing-library/react';
import PropTypes from 'prop-types';

import ContentTagsCollapsible from './ContentTagsCollapsible';
import messages from './messages';
import { useTaxonomyTagsData } from './data/apiHooks';

jest.mock('./data/apiHooks', () => ({
useTaxonomyTagsDataResponse: jest.fn(),
useIsTaxonomyTagsDataLoaded: jest.fn(),
useContentTaxonomyTagsMutation: jest.fn(() => ({
isError: false,
mutate: jest.fn(),
})),
useTaxonomyTagsData: jest.fn(() => ({
isSuccess: false,
data: {},
})),
}));

const data = {
id: 123,
name: 'Taxonomy 1',
contentTags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
},
],
contentId: 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab',
taxonomyAndTagsData: {
id: 123,
name: 'Taxonomy 1',
contentTags: [
{
value: 'Tag 1',
lineage: ['Tag 1'],
},
{
value: 'Tag 1.1',
lineage: ['Tag 1', 'Tag 1.1'],
},
{
value: 'Tag 2',
lineage: ['Tag 2'],
},
],
},
editable: true,
};

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

ContentTagsCollapsibleComponent.propTypes = {
contentId: PropTypes.string.isRequired,
taxonomyAndTagsData: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
Expand All @@ -40,22 +62,155 @@ ContentTagsCollapsibleComponent.propTypes = {
lineage: PropTypes.arrayOf(PropTypes.string),
})),
}).isRequired,
editable: PropTypes.bool.isRequired,
};

describe('<ContentTagsCollapsible />', () => {
it('should render taxonomy tags data along content tags number badge', async () => {
await act(async () => {
const { container, getByText } = render(<ContentTagsCollapsibleComponent taxonomyAndTagsData={data} />);
const { container, getByText } = render(
<ContentTagsCollapsibleComponent
contentId={data.contentId}
taxonomyAndTagsData={data.taxonomyAndTagsData}
editable={data.editable}
/>,
);
expect(getByText('Taxonomy 1')).toBeInTheDocument();
expect(container.getElementsByClassName('badge').length).toBe(1);
expect(getByText('2')).toBeInTheDocument();
expect(getByText('3')).toBeInTheDocument();
});
});

it('should render new tags as they are checked in the dropdown', async () => {
useTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
results: [{
value: 'Tag 1',
subTagsUrl: null,
}, {
value: 'Tag 2',
subTagsUrl: null,
}, {
value: 'Tag 3',
subTagsUrl: null,
}],
},
});

await act(async () => {
const { container, getByText, getAllByText } = render(
<ContentTagsCollapsibleComponent
contentId={data.contentId}
taxonomyAndTagsData={data.taxonomyAndTagsData}
editable={data.editable}
/>,
);

// Expand the Taxonomy to view applied tags and "Add tags" button
const expandToggle = container.getElementsByClassName('collapsible-trigger')[0];
await act(async () => {
fireEvent.click(expandToggle);
});

// Click on "Add tags" button to open dropdown to select new tags
const addTagsButton = getByText(messages.addTagsButtonText.defaultMessage);
await act(async () => {
fireEvent.click(addTagsButton);
});

// Wait for the dropdown selector for tags to open,
// Tag 3 should only appear there
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
expect(getAllByText('Tag 3').length === 1);
});

const tag3 = getByText('Tag 3');
await act(async () => {
fireEvent.click(tag3);
});

// After clicking on Tag 3, it should also appear in amongst
// the tag bubbles in the tree
await waitFor(() => {
expect(getAllByText('Tag 3').length === 2);
});
});
});

it('should remove tag when they are unchecked in the dropdown', async () => {
useTaxonomyTagsData.mockReturnValue({
isSuccess: true,
data: {
results: [{
value: 'Tag 1',
subTagsUrl: null,
}, {
value: 'Tag 2',
subTagsUrl: null,
}, {
value: 'Tag 3',
subTagsUrl: null,
}],
},
});

await act(async () => {
const { container, getByText, getAllByText } = render(
<ContentTagsCollapsibleComponent
contentId={data.contentId}
taxonomyAndTagsData={data.taxonomyAndTagsData}
editable={data.editable}
/>,
);

// Expand the Taxonomy to view applied tags and "Add tags" button
const expandToggle = container.getElementsByClassName('collapsible-trigger')[0];
await act(async () => {
fireEvent.click(expandToggle);
});

// Check that Tag 2 appears in tag bubbles
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
});

// Click on "Add tags" button to open dropdown to select new tags
const addTagsButton = getByText(messages.addTagsButtonText.defaultMessage);
await act(async () => {
fireEvent.click(addTagsButton);
});

// Wait for the dropdown selector for tags to open,
// Tag 3 should only appear there, (i.e. the dropdown is open, since Tag 3 is not applied)
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
});

// Get the Tag 2 checkbox and click on it
const tag2 = getAllByText('Tag 2')[1];
await act(async () => {
fireEvent.click(tag2);
});

// After clicking on Tag 2, it should be removed from
// the tag bubbles in so only the one in the dropdown appears
expect(getAllByText('Tag 2').length === 1);
});
});

it('should render taxonomy tags data without tags number badge', async () => {
data.contentTags = [];
const updatedData = { ...data };
updatedData.taxonomyAndTagsData.contentTags = [];
await act(async () => {
const { container, getByText } = render(<ContentTagsCollapsibleComponent taxonomyAndTagsData={data} />);
const { container, getByText } = render(
<ContentTagsCollapsibleComponent
contentId={updatedData.contentId}
taxonomyAndTagsData={updatedData.taxonomyAndTagsData}
editable={updatedData.editable}
/>,
);
expect(getByText('Taxonomy 1')).toBeInTheDocument();
expect(container.getElementsByClassName('invisible').length).toBe(1);
});
Expand Down
28 changes: 9 additions & 19 deletions src/content-tags-drawer/ContentTagsDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import messages from './messages';
import ContentTagsCollapsible from './ContentTagsCollapsible';
import { extractOrgFromContentId } from './utils';
import {
useContentTaxonomyTagsDataResponse,
useIsContentTaxonomyTagsDataLoaded,
useContentDataResponse,
useIsContentDataLoaded,
useContentTaxonomyTagsData,
useContentData,
} from './data/apiHooks';
import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/data/apiHooks';
import Loading from '../generic/Loading';
Expand All @@ -24,26 +22,17 @@ const ContentTagsDrawer = () => {

const org = extractOrgFromContentId(contentId);

const useContentData = () => {
const contentData = useContentDataResponse(contentId);
const isContentDataLoaded = useIsContentDataLoaded(contentId);
return { contentData, isContentDataLoaded };
};

const useContentTaxonomyTagsData = () => {
const contentTaxonomyTagsData = useContentTaxonomyTagsDataResponse(contentId);
const isContentTaxonomyTagsLoaded = useIsContentTaxonomyTagsDataLoaded(contentId);
return { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded };
};

const useTaxonomyListData = () => {
const taxonomyListData = useTaxonomyListDataResponse(org);
const isTaxonomyListLoaded = useIsTaxonomyListDataLoaded(org);
return { taxonomyListData, isTaxonomyListLoaded };
};

const { contentData, isContentDataLoaded } = useContentData();
const { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded } = useContentTaxonomyTagsData();
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
const {
data: contentTaxonomyTagsData,
isSuccess: isContentTaxonomyTagsLoaded,
} = useContentTaxonomyTagsData(contentId);
const { taxonomyListData, isTaxonomyListLoaded } = useTaxonomyListData();

const closeContentTagsDrawer = () => {
Expand Down Expand Up @@ -113,7 +102,8 @@ const ContentTagsDrawer = () => {
{ isTaxonomyListLoaded && isContentTaxonomyTagsLoaded
? taxonomies.map((data) => (
<div key={`taxonomy-tags-collapsible-${data.id}`}>
<ContentTagsCollapsible taxonomyAndTagsData={data} />
{/* TODO: Properly set whether tags should be editable or not based on permissions */}
<ContentTagsCollapsible contentId={contentId} taxonomyAndTagsData={data} editable />
<hr />
</div>
))
Expand Down
Loading