-
Notifications
You must be signed in to change notification settings - Fork 81
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
[FC-0036] feat: Add content tags tree state + editing #704
[FC-0036] feat: Add content tags tree state + editing #704
Conversation
Thanks for the pull request, @yusuf-musleh! Please note that it may take us up to several weeks or months to complete a review and merge your PR. Feel free to add as much of the following information to the ticket as you can:
All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here. Please let us know once your PR is ready for our review and all tests are green. |
41879d1
to
f40f1ea
Compare
|
||
// Add tag to the tree, and while traversing remove any selected ancestor tags | ||
// as they should become implicit | ||
const addTags = (tree, tagLineage, selectedTag) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any handler functions should be wrapped in the React.useCallback
hook
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this function is being used inside the handler, does it also need to be wrapped in the React.useCallback if the handler itself is wrapped?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, no then it's not necessary... but in that case I would personally just move this function outside of the component altogether to become a top-level function.
BTW the rule generally is if you're using a function as a prop value, it should be wrapped in a hook. The point of a hook like useCallback
is that if it's called multiple times, it still returns the exact same function (same instance), and it only re-declares the function if the array of dependencies changes. Otherwise, every time your parent component is rendered, a new function instance is initialized and when you pass it as a prop, react considers it a changed prop (it's an identical function but a different instance, so it's not the same by reference), and then will have to re-render the child component. And as you saw, sometimes the variables captured within your function get out of sync with the components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, the addTags
function uses dispatchers from the useCheckboxSetValues
so I left it there, but I moved the other functions outside the component. 7c02136
55d5385
to
5929062
Compare
|
||
/** | ||
* Builds the mutation to update the tags applied to the content object | ||
* @returns {import("./types.mjs").UseMutationResult} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we delete this line @returns {import("./types.mjs").UseMutationResult}
and the custom UseMutationResult
type? TypeScript can infer the correct type from the react-query code, and the full type is much more useful/accurate.
Without that line you actually get the full, accurate type:
No need for useContentTaxonomyTagsDataResponse
Also, there is a similar issue above. The return type of useContentTaxonomyTagsData
is wrong, and should be set to @returns {import("@tanstack/react-query").UseQueryResult<import("./types.mjs").ContentTaxonomyTagsData>}
.
If you do that, you'll see there's no need for the useContentTaxonomyTagsDataResponse
function at all.
Instead of
const useContentTaxonomyTagsData = () => {
const contentTaxonomyTagsData = useContentTaxonomyTagsDataResponse(contentId);
const isContentTaxonomyTagsLoaded = useIsContentTaxonomyTagsDataLoaded(contentId);
return { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded };
};
const { contentTaxonomyTagsData, isContentTaxonomyTagsLoaded } = useContentTaxonomyTagsData();
With the accurate types in place, you can just write:
const {data, isSuccess} = useContentTaxonomyTagsData(...);
Or if you want to rename those variables, write it as:
const {data: contentTaxonomyTagsData, isSuccess: isContentTaxonomyTagsLoaded} = useContentTaxonomyTagsData(...);
which is exactly the same thing as the code you have now, but with way less boilerplate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! Thanks for the detailed breakdown. That's definitely much simpler and cleaner, I updated all of them to follow this structure: bdc8d30
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, much simpler :)
@@ -109,3 +114,13 @@ export const useContentDataResponse = (contentId) => { | |||
export const useIsContentDataLoaded = (contentId) => ( | |||
useContentData(contentId).status === 'success' | |||
); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In useContentData
(above, but I can't comment directly on the right line), the query key is wrong. It needs to include the contentId, so a good query key would be ['contentData', contentId]
. Otherwise the data about XBlock B will be read from the cache of XBlock A and users will see the wrong data.
Same for useContentTaxonomyTagsData
- the query key needs to include the content ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, makes sense, I updated it. ec2c842
*/ | ||
export const useContentTaxonomyTagsMutation = () => ( | ||
useMutation({ | ||
mutationFn: ({ contentId, taxonomyId, tags }) => updateContentTaxonomyTags(contentId, taxonomyId, tags), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's how to get the right typing for mutationFn
:
- Remove the incomplete
@returns
type as mentioned above. - Add a type declaration to
mutationFn
of@type {import("@tanstack/react-query").MutateFunction<any, any, {contentId: string, taxonomyId: number, tags: string[]}>}
as seen in the screenshot below. - Now all the types are accurate - TypeScript can infer the rest automatically. You can see in the screenshot that when you go to call
mutate()
it knows what arguments you need.
Note: I think taxonomyId
should be a number, but you can do it as a string if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated as well (also changed ittaxonomyId
to a number), bdc8d30
14722f9
to
313728b
Compare
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #704 +/- ##
=======================================
Coverage 88.85% 88.85%
=======================================
Files 468 469 +1
Lines 7168 7252 +84
Branches 1539 1558 +19
=======================================
+ Hits 6369 6444 +75
- Misses 772 781 +9
Partials 27 27 ☔ View full report in Codecov by Sentry. |
b82ce90
to
63e965c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
- I tested this: I followed the testing instructions.
- I read through the code and considered the security, stability and performance implications of the changes.
- I tested that the UI can be used with a keyboard only (tab order, keyboard controls).
- Includes tests for bugfixes and/or features added.
- Includes documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor React hook usage tips...
if (destinationValue && sourceValue && typeof destinationValue === 'object' && typeof sourceValue === 'object') { | ||
mergeRecursively(destinationValue, sourceValue); | ||
} else { | ||
// eslint-disable-next-line no-param-reassign |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think this lint error was here for good reason. The current implementation is modifying the input objects, which I don't think it should.
const orig = {
c: {label: "C", children: {}},
a: {label: "A", children: {}},
b: {label: "B", children: {
c: {label: "BC", children: {}},
a: {label: "BA", children: {}},
b: {label: "BB", children: {}},
}},
};
const changed = mergeTrees(orig, orig);
console.log(orig);
If you run this (e.g. in your browser console), you'll see that orig
gets unexpectedly modified by this, and its b.children
have become sorted in-place.
If you run
const orig = {
c: {label: "C", children: {}},
a: {label: "A", children: {}},
b: Object.freeze({label: "B", children: {
c: {label: "BC", children: {}},
a: {label: "BA", children: {}},
b: {label: "BB", children: {}},
}}),
};
const changed = mergeTrees(orig, orig);
you'll get an error when it tries to modify the object b
in-place instead of creating a copy of it and modifying that.
It's better to use immutable data structures when possible so although this may not be causing any bugs now, I'd recommend tweaking it to not modify the input objects in any way so that this function behaves more consistently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bradenmacdonald Thanks for catching this very intricate bug! Though the main issue wasn't the reassigning of the param, as the linter mentions, since the destination
is always copied over (each level of the recursion), however the issue was that the source
tree/value was not, hence it was getting modified.
I tried copying over the source value (tree) in the same style (using { ...sourceValue }
) however that made it really slow. I realize we have lodash
installed, and it has a much more efficient clone method, so I used that instead, and it works great, the input objects no longer get modified. 9fd8c6e
const intl = useIntl(); | ||
const { | ||
id, name, contentTags, | ||
} = taxonomyAndTagsData; | ||
|
||
// State to determine whether to update the backend or not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit/docs: Just from reading this comment, I'm still unclear on what this state is doing, without reading more of the code. What does it mean "to determine whether to update the backend or not" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. It essentially determines whether we should make a call to the update endpoint. I wrote a more descriptive comment: a5087dd
// however if it is null that means it is the root, and the apiHooks | ||
// would automatically handle it. Later this url is set to the next | ||
// page of results (if any) | ||
const [fetchUrl, setFetchUrl] = useState(subTagsUrl); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fetchUrl
pattern is a bit weird to me. I would suggest putting in a comment that for the future, we'd probably want to refactor to having a count of how many times the user has clicked "load more" and then use useQueries
to load all the pages based on that. No need to refactor now though if this is working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah fetchUrl
feels a bit overloaded and is doing too much at the moment. I agree it warrants a refactor, I added a TODO
comment to address it in the future.
if (!implicit && editable) { | ||
removeTagHandler(lineage.join(','), false); | ||
} | ||
}, [implicit, lineage]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs editable
and removeTagHandler
as dependencies too, since you use those prop variables inside this callback. I'm surprise a react hook linter didn't catch this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the exhaustive-deps
option is turned off for the linter, not really sure why. That's why it didn't catch it, when I turned it on the linter pointed it out.
I included the missing dependancies.
4bdb8e8
to
a5087dd
Compare
@yusuf-musleh Can you please add [FC-0036] to the PR title? |
@bradenmacdonald Sure thing |
@yusuf-musleh If this is hard to fix, it can be a future PR, but the "add tags" widget is still moving around awkwardly when I click on tags: recording.mov |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than the above issue, I'm happy with the code now. Thanks! We'll continue to refine and improve this in future PRs.
@bradenmacdonald hmm, I looked into it a bit. The problem is that the dropdown is "attached" to the "add tags" button, so it moves with it when the new tags are added causing this awkward movement. Keeping it in places however, I feel, would be more awkward as the button moves further away and the dropdown would be "floating" by itself. I think the proper fix would be, as you and @ali-hugo mentioned before, to have the tags added after the dropdown is closed, that should properly fix the issue. I can work on as part of the UI/UX refinements PR. |
@yusuf-musleh Sounds good. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think not keeping a tree structure, and embracing recursive components might simplify this code quite a bit.
// State to determine whether the tags are being updating so we can make a call | ||
// to the update endpoint to the reflect those changes | ||
const [updatingTags, setUpdatingTags] = React.useState(false); | ||
const mutation = useContentTaxonomyTagsMutation(contentId, id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not specific to this PR but to most the PRs using useQuery
. I think a term like mutation is pretty unclear. I understand that is the preference of this framework, however, it's not clear from this function what it's doing.
I think what might be nice, is to change this name to useContentTaxonomyTagsUpdater
, and have that directly require the function to call for the update. i.e.
`
const updateTags = useContentTaxonomyTagsUpdate(contentId, id);
...
updateTags({ tags });
That way someone who's not familiar with the framework can still understand what is going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense, I've renamed it for clarity, however I kept return object as is, since the mutation object contains methods/properties (such as isError
and reset()
) that I make use of through out the code.
|
||
const handleSelectableBoxChange = React.useCallback((e) => { | ||
tagChangeHandler(e.target.value, e.target.checked); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of code in this component before the UI part of this component starts. In situations like this, I think it can be clearer to extract a hook out of it so that the logic and UI are less intertwined.
For example, you could extract the following react hook out of this component:
const useTagCollapsableHelper = (contentId, taxonomyAndTagsData) => {
const {
id, contentTags,
} = 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
const [updatingTags, setUpdatingTags] = React.useState(false);
const mutation = useContentTaxonomyTagsMutation(contentId, id);
// Keeps track of the content objects tags count (both implicit and explicit)
const [contentTagsCount, setContentTagsCount] = React.useState(0);
// Keeps track of the tree structure for tags that are add by selecting/unselecting
// tags in the dropdowns.
const [addedContentTags, setAddedContentTags] = React.useState({});
// To handle checking/unchecking tags in the SelectableBox
const [checkedTags, { add, remove, clear }] = useCheckboxSetValues();
// Handles making requests to the update endpoint whenever the checked tags change
React.useEffect(() => {
// We have this check because this hook is fired when the component first loads
// and reloads (on refocus). We only want to make a request to the update endpoint when
// the user is updating the tags.
if (updatingTags) {
setUpdatingTags(false);
const tags = checkedTags.map(t => decodeURIComponent(t.split(',').slice(-1)));
mutation.mutate({ tags });
}
}, [contentId, id, checkedTags]);
// This converts the contentTags prop to the tree structure mentioned above
const appliedContentTags = React.useMemo(() => {
let contentTagsCounter = 0;
// Clear all the tags that have not been commited and the checked boxes when
// fresh contentTags passed in so the latest state from the backend is rendered
setAddedContentTags({});
clear();
// When an error occurs while updating, the contentTags query is invalidated,
// hence they will be recalculated, and the mutation should be reset.
if (mutation.isError) {
mutation.reset();
}
const resultTree = {};
contentTags.forEach(item => {
let currentLevel = resultTree;
item.lineage.forEach((key, index) => {
if (!currentLevel[key]) {
const isExplicit = index === item.lineage.length - 1;
currentLevel[key] = {
explicit: isExplicit,
children: {},
};
// Populating the SelectableBox with "selected" (explicit) tags
const value = item.lineage.map(l => encodeURIComponent(l)).join(',');
// eslint-disable-next-line no-unused-expressions
isExplicit ? add(value) : remove(value);
contentTagsCounter += 1;
}
currentLevel = currentLevel[key].children;
});
});
setContentTagsCount(contentTagsCounter);
return resultTree;
}, [contentTags, mutation.isError]);
// This is the source of truth that represents the current state of tags in
// this Taxonomy as a tree. Whenever either the `appliedContentTags` (i.e. tags passed in
// the prop from the backed) change, or when the `addedContentTags` (i.e. tags added by
// selecting/unselecting them in the dropdown) change, the tree is recomputed.
const tagsTree = React.useMemo(() => (
mergeTrees(appliedContentTags, addedContentTags)
), [appliedContentTags, addedContentTags]);
// Add tag to the tree, and while traversing remove any selected ancestor tags
// as they should become implicit
const addTags = (tree, tagLineage, selectedTag) => {
const value = [];
let traversal = tree;
tagLineage.forEach(tag => {
const isExplicit = selectedTag === tag;
if (!traversal[tag]) {
traversal[tag] = { explicit: isExplicit, children: {} };
} else {
traversal[tag].explicit = isExplicit;
}
// Clear out the ancestor tags leading to newly selected tag
// as they automatically become implicit
value.push(encodeURIComponent(tag));
// eslint-disable-next-line no-unused-expressions
isExplicit ? add(value.join(',')) : remove(value.join(','));
traversal = traversal[tag].children;
});
};
const tagChangeHandler = React.useCallback((tagSelectableBoxValue, checked) => {
const tagLineage = tagSelectableBoxValue.split(',').map(t => decodeURIComponent(t));
const selectedTag = tagLineage.slice(-1)[0];
const addedTree = { ...addedContentTags };
if (checked) {
// We "add" the tag to the SelectableBox.Set inside the addTags method
addTags(addedTree, tagLineage, selectedTag);
} else {
// Remove tag from the SelectableBox.Set
remove(tagSelectableBoxValue);
// We remove them from both incase we are unselecting from an
// existing applied Tag or a newly added one
removeTags(addedTree, tagLineage);
removeTags(appliedContentTags, tagLineage);
}
setAddedContentTags(addedTree);
setUpdatingTags(true);
});
return { tagChangeHandler, tagsTree, contentTagsCount, checkedTags };
};
This could be put in a separate file in the component package. It'll help keep the function a bit cleaner.
This is just my preference of course if you feel otherwise do tell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I think splitting it out makes it cleaner, i've done that here: 8d1f7e9
|
||
const loadMoreTags = useCallback(() => { | ||
setFetchUrl(nextPage); | ||
}, [nextPage]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you can extract out all the pagination related code into a separate hook called: usePaginatedTaxonomyTagsData
. This can be a reusable hook that can handle all the pagination:
const usePaginatedTaxonomyTagsData = (subTagsUrl) => {
const [tags, setTags] = useState([]);
const [nextPage, setNextPage] = useState(null);
// `fetchUrl` is initially `subTagsUrl` to fetch the initial data,
// however if it is null that means it is the root, and the apiHooks
// would automatically handle it. Later this url is set to the next
// page of results (if any)
//
// TODO: In the future we may need to refactor this to keep track
// of the count for how many times the user clicked on "load more" then
// use useQueries to load all the pages based on that.
const [fetchUrl, setFetchUrl] = useState(subTagsUrl);
const { data: taxonomyTagsData, isSuccess: isTaxonomyTagsLoaded } = useTaxonomyTagsData(taxonomyId, fetchUrl);
useEffect(() => {
if (isTaxonomyTagsLoaded && taxonomyTagsData) {
setTags([...tags, ...taxonomyTagsData.results]);
setNextPage(taxonomyTagsData.next);
}
}, [isTaxonomyTagsLoaded, taxonomyTagsData]);
const loadMoreTags = useCallback(() => {
setFetchUrl(nextPage);
}, [nextPage]);
return { tags, loadMoreTags, nextPage};
}
You can then just use it as const { tags, nextPage, loadMoreTags} = usePaginatedTaxonomyTagsData(subTagsUrl);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great suggestion! Since I am refactoring the way I do pagination in the search tags PR I think it makes sense to include this improvement as part of that PR as well.
let traversal = tagsTree; | ||
lineage.forEach(t => { | ||
// We need to decode the tag to traverse the tree since the lineage value is encoded | ||
traversal = traversal[decodeURIComponent(t)]?.children || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why decodeURIComponent
and encodeURIComponent
are being used here. Why not use the direct value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because commas (,
) are used as delimiters between tags in the serialized lineage, and tags can contain contain commas. So we encode/decode to avoid any issue there. More discussions: #704 (comment)
.pgn__selectable_box:disabled, | ||
.pgn__selectable_box[disabled] { | ||
opacity: 1 !important; | ||
} | ||
|
||
.pgn__selectable_box-active { | ||
outline: none !important; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to closer match the Figma design mockups. As the default styling for the selectable box component in Paragon contains borders/greyed out text that differ from the styles in Figma
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing to note is that often these Figma designs are based on the 2U/edX theme instead of the vanilla theme. So you should try installing that branding package and then seeing what doesn't match.
Additionally, I think we can also start by commenting on the design about changing the design to match figma rather than overriding a component like this.
Note that this will apply to all instances of this component in course-authoring. Which means that other components of course-authoring will stop matching their figma designs.
If the idea is to move paragon in this direction, then that change needs to be made in paragon.
I think it's worth having this discussion and saving this change for later, till the discussion as been had.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I installed the 2U edx theme and style was still off from the Figma designs (note the spacing/alignment in the screenshots are fixed in open-craft#16):
Default Theme without overrides:
2U edx theme without overrides:
I agree, these style changes should be part of the theme or Paragon rather than overriding them here, any idea's who we can discuss this with? Maybe @ali-hugo might have some insights?
For now would it be acceptable to make the overrides more specific by adding the class so it doesn't override all these styles? cae319e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yusuf-musleh @xitij2000 CC @ali-hugo Also please remember the mockups for this project are not pixel-perfect UX designs. So you don't need to try to match them exactly; if there's an off-the-shelf Paragon component or CSS class that provides the functionality we need, just use it.
But something like you're showing above, where the outline is behind another component, definitely should be fixed.
For questions about how to improve Paragon, I find it best to ask on Slack #wg-frontend
Let's get this PR merged though; we can fine-tune the the appearance in follow-up PRs. We are in MVP mode here and this one holding up other work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I've asked about it in the #wg-frontend channel.
f35727e
to
8d1f7e9
Compare
@xitij2000 Thanks for the review! I've addressed the issues brought up. However regarding the tree structure refactor, I think I'll wait on @bradenmacdonald to weigh in on it in the ticket to see how we should proceed. |
@xitij2000 Friendly reminder for final review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good and seems to be working. My only concern is the overriding of paragon styles.
.pgn__selectable_box:disabled, | ||
.pgn__selectable_box[disabled] { | ||
opacity: 1 !important; | ||
} | ||
|
||
.pgn__selectable_box-active { | ||
outline: none !important; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing to note is that often these Figma designs are based on the 2U/edX theme instead of the vanilla theme. So you should try installing that branding package and then seeing what doesn't match.
Additionally, I think we can also start by commenting on the design about changing the design to match figma rather than overriding a component like this.
Note that this will apply to all instances of this component in course-authoring. Which means that other components of course-authoring will stop matching their figma designs.
If the idea is to move paragon in this direction, then that change needs to be made in paragon.
I think it's worth having this discussion and saving this change for later, till the discussion as been had.
@yusuf-musleh Could you please squash this to a single commit so @xitij2000 can merge? He prefers that you squash it yourself. |
This commit adds the add/remove functionality of content tags where the state is stored and changes are updated in the backend through the API. Changes are reflected in the UI automatically. style: Make custom style overrides more specific fix: ModalPopup appears cut off when top of screen refactor: Extract non-UI logic to helper component refactor: Rename mutation -> updater for clarity chore: Review nits style: Remove hover style for tag bubbles feat: Include implicit tags in tags count badge refactor: Nits from PR review fix: Copy tree to avoid modifying originals test: Add tests for new content tags editing test: Update tests for latest code fix: Fix typing issues with mutations fix: Clear checkboxes when mutation error feat: Add `editable` flag to ContentTagsDrawer feat: Invalidate query refetch content tags count refactor: Simplify react-query hooks + fix types fix: Add missing `contentId` to queryKey refactor: Move util functions outside component fix: Remove arrow from tags dropdown modal feat: "Load more" button to handle paginated tags chore: fix stylelint issues fix: Use Chip instead of Button for TagBubble feat: Add API calls to backend when updating tags feat: Add content tags tree state + editing
cae319e
to
ae22fb1
Compare
@bradenmacdonald Done, commits squashed to one. cc: @xitij2000 |
@yusuf-musleh 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
Description
This PR adds the editing functionality for content tags. You are able to select/deselect tags from the dropdowns in each taxonomy and the tags tree structure is updated, requests are sent to the backend, everything gets automatically updated and the state persists.
When a tag is select, all its ancestors automatically become 'implicit' and are unselected and cannot be selected. If an explicit tag is unselected and the parent (or ancestor) no longer has explicit children, all ancestor tags will also be removed.
Supporting information
Related Tickets:
Testing instructions
Private-ref: FAL-3530