feat: Add tags to a taxonomy#2872
Conversation
|
Thanks for the pull request, @jesperhodge! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
|
Dear reviewers, |
|
Thanks, I'm so happy to see this coming together!
Any thoughts on how to handle this? Would it be useful to use infinite scroll instead of pagination, and load sub-tags on demand when their parent tag is expanded?
This doesn't seem to be working. When I add a new top-level tag, it always appears at the top of the list, but when I later refresh, it moves to alphabetical order. I think it should immediately put the tag into the correct position and then "flash" it to highlight where it is in the list. Bugs:
Here's a bunch of UX feedback. I know you're probably aware of many of these already, and they don't have to be fixed within this PR necessarily, but it's easier for me to just list them all.
|
bradenmacdonald
left a comment
There was a problem hiding this comment.
Don't have time for a full review now, but here's some initial thoughts.
| // The table has a VIEW, DRAFT, and a PREVIEW mode. It starts in VIEW mode. | ||
| // It switches to DRAFT mode when a user edits or creates a tag. | ||
| // It switches to PREVIEW mode after saving changes, and only switches to VIEW when | ||
| // the user refreshes the page, orders a column, or navigates to a different page. | ||
| // During DRAFT and PREVIEW mode the table makes POST requests and receives | ||
| // success or failure responses. | ||
| // However, the table does not refresh to show the updated data from the backend. | ||
| // This allows us to show the newly created or updated tag in the same place without reordering. |
There was a problem hiding this comment.
I wonder if we can take a simpler approach. What do you think about having just one "mode", and using optimistic updates to inject any newly-created tags into the correct spot? That way, if/when react-query refetches data from the backend, nothing gets disrupted, and we can keep everything in sync.
(plus a toggle to track whether the user is currently creating a new tag or not)
There was a problem hiding this comment.
The modes are mostly necessary to prevent reloading data and alphabetical reloading after the user has successfully saved a new tag, so that new tags and subtags are shown at the top. See my larger comment about this criteria. In terms of optimistic updates, which are there to inject updates even before they have been saved, that would prevent tags to show up in the spot we want, and it doesn't align with our AC, which is to show error messages when the tag does not successfully save and not display the tag optimistically hoping for successful save.
There was a problem hiding this comment.
Can I resolve this conversation?
There was a problem hiding this comment.
I still think the modes are adding a lot of unnecessary complexity. Couldn't we achieve the same thing without these modes, and a much simpler isCurrentlyEditingTag and lastCreatedTag state that correspond to the "draft mode" and "preview mode" respectively?
If you track those two states, then it doesn't matter if/when React query reloads data, and you don't have to have special case behavior or state transitions, or anything else. You just make sure that if there is currently a lastCreatedTag (you'd track both the tag and its parent ID in the state), then it gets "hidden" from its normal place and rendered directly below its parent. I think that would achieve all the AC you have.
You could also do it with optimistic updates too; just because you're using optimistic updates doesn't mean you can't insert the new tag into the desired place (first on the list) or handle errors when they eventually come through. But in that case, it would be more work to avoid the eventual shift into alphabetical order whenever React query does refresh the list, which is trivially avoided by the state approach I suggested above.
I know you're short on time here so I won't block the PR on this but I do think it should be at least marked as a TODO to explore, as a way of simplifying this.
There was a problem hiding this comment.
I love these ideas, and put them into a TODO. I would also like to simplify the table as much as possible.
I'd like to explore these ideas more when I find some time in the future.
One question: are you sure these two ideas make the table less complex? My take at least with states like isCurrentlyEditing is that the more states like this we have, the more complex the situation becomes. My goal with the table modes was to abstract from these details and just make the table behavior easy to predict and very robust, that's why the state machine and the safeguards. The overall TagListTable file is now very short and has little state and few useEffects, making the lifecycle quite simple.
I agree that the modes make things less simple, and if we can get rid of them and simplify I would be glad. But wouldn't either of the suggested solutions introduce more complicated table states?
There was a problem hiding this comment.
By the way I'm not meaning to push back on your ideas, I'm just brainstorming to figure out what the best solution would be. I'm principally not opposed to either of your suggestions.
There was a problem hiding this comment.
I guess I was hoping it would be much less complicated, and the only differences in behavior would be limited to the NestedRows component, but I'll defer to your opinion on it.
The main thing about lastCreatedTag is it doesn't have any global effect; it just affects the parent of the lastCreatedTag (which you'd store in the state), so that within the NestedRows component, you'd have a check that re-orders a local copy of its childRowsData if parentRowValue == lastCreatedTag.parentValue.
So I'm thinking it would just be a few lines of code in <NestedRows>, but maybe I am missing some other behavior that we need.
There was a problem hiding this comment.
Let's say I add a new top-level tax "X-man". Then I want to edit this in place not search for it on some other page or at the bottom. Then I add some child tags and some grand child tags to it, and all the while I don't want things to change their place so I can edit it.
Or I just rename an existing parent tag and then I want to make changes to some of its 200 child tags but not others, and also not be interrupted by constant reordering.
How do we account for that? Isn't a display-only data structure, in this case tagTree, the way to handle it?
There was a problem hiding this comment.
Then instead of a singular lastCreatedTag, you'd want an array, tagsEditedThisSession = []. I think it would mostly be the same though - a few lines of code in NestedRows to re-order its children to the top if any of them are in the list. (But all the new tags would be in alphabetical order.)
Or for slightly more complex behavior, you have a tagsEditedThisSession = {[tagValue]: timestamp} data structure, and then you can just make <NestedRows> so that it always re-orders its children first by (look up editTimestamp in tagsEditedThisSession if any) then by (tag value). I think this would give all the properties you have specified without any if statements or modes whatsoever. And as long as the sorting was memoized it would be very performant too. I guess this isn't too different from what you're doing with building a whole separate data structure, but I think it's a simpler approach and easier to understand.
That said, if your tagTree data structure is annotating each tag with additional data that goes beyond its local react state, it's probably necessary. I don't think I've seen any such use case yet though.
not be interrupted by constant reordering
I know neither of us are the product decision makers here, but personally I would strongly prefer a UX that shows the tags in place, and just highlights newly created tags with a NEW label / highlight color. I think that always sorting them into place is more consistent.
I just tested this now in MacOS Finder, and it works exactly how I'd expect the tag tree to work: if you have a folder sorted by name and rename or create a folder, as soon as you give it a name it jumps into place in the sorted order but remains highlighted and focused. This is just sensible IMHO.
Screen.Recording.2026-03-26.at.1.20.05.PM.mov
There was a problem hiding this comment.
I like the option we see in your video as well, I have no preference between the two UX designs. I'm just following the Acceptance Criteria in what was communicated to me as "the ideal behavior". I can't do anything about that.
Re: tagsEditedThisSession array - maybe I'm wrong, but seems to make things pretty confusing to check in each branch or table row whether a tag should be rendered there or not depending on this array, when the data that's for rendering the tree says something contradicting it. Sounds to me like instead of an array we should just have a tree data structure that holds the table state and correctly reflects it, so you always know what the state is, what it should be, and when it gets refreshed from the backend?
In terms of complexity, what exactly is it that seems complex to you? I also have the impression that this is overcomplicated, but I cannot pinpoint where. Maybe instead of three modes we can just have a preventDataReload state for the table? It would still need to be clearly shown when the table can reload data and when not, so I was a bit concerned that this will create confusion and bugs, so I went with 3 clear modes instead. But I'm wondering now what can improve here.
I'm mostly concerned with simplicity, readability and separation of concerns here, so the only thing that really sticks out for me is that I would ideally like to have a full separation of concerns between the tags as in tag-list and the tree-table, making the tree-table reusable. And move the tagTree to the tree-table so it doesn't depend on tags.
That was my plan for further improvement, but it's a bit of work, so I couldn't do it within this PR.
|
@bradenmacdonald thanks for the very helpful feedback! Pagination:
Tags ordering / Preview:
I implemented that as we decided for the ACs. The current way it works is indeed that it always appears at the top of the list, but on refresh moves to alphabetical order. Some of the reasons we landed on these ACs were:
I brought your recommendation about positioning and flashing to the team, and we are going to raise this concern to the product owner, Jenna, to get her input. Bugs:
UX:
Let me know if you have any questions! Your feedback is very valuable to us. |
| queryKey: taxonomyQueryKeys.taxonomyTagListPage(taxonomyId, pageIndex, pageSize), | ||
| queryFn: async () => { | ||
| const { data } = await getAuthenticatedHttpClient().get(apiUrls.tagList(taxonomyId, pageIndex, pageSize)); | ||
| const { data } = await getAuthenticatedHttpClient().get(apiUrls.tagList(taxonomyId, pageIndex, pageSize, 1000)); |
There was a problem hiding this comment.
What is the "1000" here? Do we have a place to make this a constant somewhere?
There was a problem hiding this comment.
It's pretty arbitrary, it's the recommendation in the API docs if you want to request all depths (since there's no "infinite"). I'll just add a comment that this fetches full depth if that's fine.
There was a problem hiding this comment.
@jesperhodge I agree that we may want to make this a constant in each of the places it's used.
| > | ||
| <Layout.Element> | ||
| <TagListTable taxonomyId={taxonomyId} /> | ||
| <TagListTable taxonomyId={taxonomyId} maxDepth={3} /> |
There was a problem hiding this comment.
Is there a place to move this to a configuration so that if we want to change the depth it doesn't require a code change?
There was a problem hiding this comment.
Yes, I'll move that to constants
There was a problem hiding this comment.
Done, can we resolve this conversation?
If we implement "flashing", we could also implement "change to the correct page".
It's fine with me if you add the option to paginate by 0th-level tags, but I think it may need to be optional, in order to preserve the API compatibility. The current API allows you to quickly load the entire taxonomy into memory by requesting the full depth and as many pages as you need until it's complete, and I think that's another option to consider here unless we think there will be taxonomies too large to performantly display in a react-table. |
| // The table has a VIEW, DRAFT, and a PREVIEW mode. It starts in VIEW mode. | ||
| // It switches to DRAFT mode when a user edits or creates a tag. | ||
| // It switches to PREVIEW mode after saving changes, and only switches to VIEW when | ||
| // the user refreshes the page, orders a column, or navigates to a different page. | ||
| // During DRAFT and PREVIEW mode the table makes POST requests and receives | ||
| // success or failure responses. | ||
| // However, the table does not refresh to show the updated data from the backend. | ||
| // This allows us to show the newly created or updated tag in the same place without reordering. |
There was a problem hiding this comment.
I guess I was hoping it would be much less complicated, and the only differences in behavior would be limited to the NestedRows component, but I'll defer to your opinion on it.
The main thing about lastCreatedTag is it doesn't have any global effect; it just affects the parent of the lastCreatedTag (which you'd store in the state), so that within the NestedRows component, you'd have a check that re-orders a local copy of its childRowsData if parentRowValue == lastCreatedTag.parentValue.
So I'm thinking it would just be a few lines of code in <NestedRows>, but maybe I am missing some other behavior that we need.
538d02e to
f193edb
Compare
|
Hi @bradenmacdonald , I did my very best to address everything. There's some weirdness now with the input autofocus or something, where when you type it will mark the first letter. Maybe that's just me, or a little bug. However this is so minor that I don't want to block the PR, get it merged, and fix that later. |
|
Thanks @jesperhodge. Once I've approved it and the build is green, shall I go ahead and merge it? |
@bradenmacdonald Yes! Thank you, please merge this. I just merged master into it once to make sure it's all ready for merge. Pipeline was green just before that so it should be fine. I'll be so happy when this is merged! |
I am seeing this too. When I go to add a new tag, and type in a name, the first letter "disappears". It's quite annoying so please try to fix it ASAP as a fast follow, if you have time. |


Description
This addresses Modular-learning #132: Adding functionality to create tags from a taxonomy list.
Go to /authoring/taxonomy, open a taxonomy, and you should be able to create new tags.
Acceptance Criteria
Found in Modular-learning #132
Architecture
The previously used Paragon DataTable is not designed to allow in-line edit functionality or work well with trees / deeply nested table entries. So I used tanstack/react-table directly to build a new tree-table that is editable inline.
AI Usage
To speed things up, I have heavily worked with Github Copilot. I have reviewed all the code carefully, but I want to point that out for awareness when it comes to review. I created pretty exhaustive tests to ensure that the code works as expected.
What is not in scope