-
Notifications
You must be signed in to change notification settings - Fork 80
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: Taxonomy delete dialog [FC-0036] #684
feat: Taxonomy delete dialog [FC-0036] #684
Conversation
Thanks for the pull request, @ChrisChV! 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. |
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## master #684 +/- ##
==========================================
+ Coverage 88.86% 88.93% +0.06%
==========================================
Files 469 472 +3
Lines 7255 7327 +72
Branches 1559 1564 +5
==========================================
+ Hits 6447 6516 +69
- Misses 781 784 +3
Partials 27 27 ☔ View full report in Codecov by Sentry. |
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 job on this, it works great! I just had a few comments/nits.
- I tested this: (ran it locally following instructions)
- I read through the code
- I checked for accessibility issues
-
Includes documentation -
I made sure any change in configuration variables is reflected in the corresponding client'sconfiguration-secure
repository.
src/taxonomy/data/apiHooks.jsx
Outdated
export const useDeleteTaxonomy = () => { | ||
/** | ||
* Deletes a taxonomy. | ||
* | ||
* @param {import("./types.mjs").DeleteTaxonomyParams} params - Parameters for deleting the taxonomy. | ||
* @returns {Promise<void>} - A promise that resolves when the taxonomy is deleted. | ||
*/ | ||
const deleteTaxonomyWrap = async ({ pk }) => { | ||
await deleteTaxonomy(pk); | ||
}; | ||
|
||
return useMutation({ | ||
mutationFn: deleteTaxonomyWrap, | ||
}); | ||
}; |
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.
Is there a reason why we dont use the deleteTaxonomy
api function directly inside the useMutation
without wrapping it? Maybe something similar to this (It might need some adjustments, as I haven't tested this, but I had something similar working in my PR)
export const useDeleteTaxonomy = () => { | |
/** | |
* Deletes a taxonomy. | |
* | |
* @param {import("./types.mjs").DeleteTaxonomyParams} params - Parameters for deleting the taxonomy. | |
* @returns {Promise<void>} - A promise that resolves when the taxonomy is deleted. | |
*/ | |
const deleteTaxonomyWrap = async ({ pk }) => { | |
await deleteTaxonomy(pk); | |
}; | |
return useMutation({ | |
mutationFn: deleteTaxonomyWrap, | |
}); | |
}; | |
/** | |
* Deletes a taxonomy. | |
*/ | |
export const useDeleteTaxonomy = () => ( | |
useMutation({ | |
mutationFn: ({ pk }) => deleteTaxonomy(pk), | |
}); | |
); |
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 I did this changes, a little copy of your PR:
/**
* Builds the mutation to delete a taxonomy.
* @returns {Object} - An object with the mutation configuration.
*/
export const useDeleteTaxonomy = () => {
const queryClient = useQueryClient()
/**
* @type {import("@tanstack/react-query").MutateFunction<any, any, {pk: number}>}
*/
return useMutation({
mutationFn: async ({ pk }) => await deleteTaxonomy(pk),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] });
},
});
};
But I got this error after run npm run types
:
src/taxonomy/data/apiHooks.jsx:40:26 - error TS2339: Property 'pk' does not exist on type 'void'.
40 mutationFn: async ({ pk }) => await deleteTaxonomy(pk),
How did you resolve this error in your PR?
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.
@ChrisChV Interesting, I actually have the same issue, but didn't show up in the PR because it is failing earlier on other issues. I tried different things to fix this, but it didnt seem to work. Maybe @bradenmacdonald might have some suggestions?
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.
@ChrisChV I figured it out, you have to put the jsdoc comment right above the mutationFn:
line, mine looks like this, (notice how it is inside the useMutation
):
/**
* Builds the mutation to update the tags applied to the content object
* @param {string} contentId The id of the content object to update tags for
* @param {number} taxonomyId The id of the taxonomy the tags belong to
*/
export const useContentTaxonomyTagsMutation = (contentId, taxonomyId) => {
const queryClient = useQueryClient();
return useMutation({
/**
* @type {import("@tanstack/react-query").MutateFunction<
* any,
* any,
* {
* tags: string[]
* }
* >}
*/
mutationFn: ({ tags }) => updateContentTaxonomyTags(contentId, taxonomyId, tags),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTags', contentId] });
},
});
};
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.
Thanks! It works
src/taxonomy/TaxonomyListPage.jsx
Outdated
useEffect(() => { | ||
const deleted = searchParams.get('deleted'); | ||
if (deleted !== null) { | ||
setToastText(intl.formatMessage(messages.taxonomyDeleteToast, { name: deleted })); | ||
setShowToast(true); | ||
searchParams.delete('deleted'); | ||
setSearchParams(searchParams); | ||
} | ||
}, []); |
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.
If I understand correctly, this is temporarily using the search query to store the item to be deleted. I'm not sure why this is needed. Why not just store this in react state?
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 added a comment on this code. You can delete taxonomies from the Taxonomy details page, so after delete, is redirected to the list page and show the toast. Using the searchParams
is the simplest way to show the toast in the redirect that I found.
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.
If you use search params, you need to make sure to use the router to reset the search params after the toast is displayed. Otherwise, if users bookmark that URL or refresh the page, they'll see the toast again. (This may sound unlikely but it happens to me all the time in Studio with the similar query param that focuses on the unit name field to let you rename a newly-created unit; it happens to me many times when the unit already exists. Very annoying.)
You could also just use localStorage to indicate that a toast should be shown though? Probably easier. Could also as on #wg-frontend if there is a standard pattern for this in our MFEs. Maybe a helper for these sort of flash messages already exists in openedx/frontend-platform or similar.
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! I used localStorage. I didn't find a standard way, but I still left my question in #wg-frontend
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.
When dealing with notifications on other projects, I used this: https://notistack.com/features/basic
If we're not able to import the library, we could use the same concept: a notification provider/context on top of our <App />
(or <TaxonomyLayout>
if we don't want affect other parts of the framework).
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 would prefer to avoid adding dependencies if at all possible; the bundle size is already very large.
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.
Local storage is good, but I'm wondering why this needs even that. Perhaps I am mistaken about this, but there is not hard refresh when navigating here, so it should be entirely possible to store this in react or redux state right?
I think these PRs are avoiding redux but even with react you can use a shared context. i.e. the root of the page can be wrapped in a which can be a component that provides common context that all components might need. See an example here.
So this context provider can be provided with the current deleted taxonomy state, i.e. what you're doing with localstorage right now.
Here is some sample code:
Context component
export const TaxonomyContext = React.createContext({
deletedTaxonomy: null,
setDeletedTaxonomy: null,
})
New root page for all taxonomy pages:
function TaxonomyRoutes(){
const [deletedTaxonomy, setDeletedTaxonomy] = useState(null);
return (
<TaxonomyContext.Provider value={{deletedTaxonomy, setDeletedTaxonomy}}>
... taxonomy routes
</TaxonomyContext.Provider>
)
}
Using the context:
const {deletedTaxonomy} = useContext(TaxonomyContext);
With this approach you can pass around data between components, and it may be useful in the future as well.
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 also need the same functionality to show a toast after importing a taxonomy.
I suggest we make this more generic, like:
const [toastMessages, setToastMessages] = useState([]);
We have the TaxonomyLayout
that is a "root" for the taxonomy pages and can be responsible to render toast/alerts.
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 like this approach I'm going to try it. Thanks to all!
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.
@xitij2000 Updated here 18a224a. Thanks for the feedback, that change will be help in the future. It's ready for another review round
let enabledMenuItems = menuItems; | ||
if (taxonomy.systemDefined) { | ||
enabledMenuItems = systemDefinedMenuItems; | ||
} |
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.
Don't we also need to check whether the user is allowed to delete this taxonomy before we show 'Delete' as a menu option?
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.
@pomegranited You are right. It may be best to do this in a follow-up task. Since the menu has been refactored and there will be conflicts. It would be best to do this after the conflicts have been resolved. CC @bradenmacdonald
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.
The code generally looks good. I've left a few nits. I haven't been able to test it yet since I don't have it set up on this computer. If it's been tested after the latest changes then it's good to go.
For the tests I don't know if it's a good idea to test the hooks the way they are being tested, i.e. being called as direct functions.
I think it's better to mock the API response calls using axiosMock and then see if the endpoints are being called.
src/taxonomy/TaxonomyListPage.jsx
Outdated
|
||
const TaxonomyListPage = () => { | ||
const intl = useIntl(); | ||
const [deletedTaxonomyName, setDeletedTaxonomyName] = useState(''); | ||
const deleteMutation = useDeleteTaxonomy(); |
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 for someone unfamiliar with useQuery this approach of getting a deleteMutation object and then calling mutate is a bit confusing. I think this logic can be moved into the hook itself. i.e. then you can use it as follows:
const deleteTaxonomy = useDeleteTaxonomyHelper();
const onDeleteTaxonomy = (id name) => {
...
deleteTaxonomy(id);
}
Essentially hide the useQuery
API from the component.
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 this will be solved using the Braden's approach
src/taxonomy/data/apiHooks.jsx
Outdated
/** | ||
* Builds the mutation to delete a taxonomy. | ||
* @returns {Object} - An object with the mutation configuration. | ||
*/ | ||
export const useDeleteTaxonomy = () => { | ||
const queryClient = useQueryClient(); | ||
return useMutation({ | ||
/** | ||
* @type {import("@tanstack/react-query").MutateFunction< | ||
* any, | ||
* any, | ||
* { | ||
* pk: number | ||
* } | ||
* >} | ||
*/ | ||
mutationFn: async ({ pk }) => deleteTaxonomy(pk), | ||
onSettled: () => { | ||
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] }); | ||
}, | ||
}); | ||
}; | ||
|
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 hiding the specifics of useQuery will make it easier for those dealing with other aspects of this code to understand what's going on.
/** | |
* Builds the mutation to delete a taxonomy. | |
* @returns {Object} - An object with the mutation configuration. | |
*/ | |
export const useDeleteTaxonomy = () => { | |
const queryClient = useQueryClient(); | |
return useMutation({ | |
/** | |
* @type {import("@tanstack/react-query").MutateFunction< | |
* any, | |
* any, | |
* { | |
* pk: number | |
* } | |
* >} | |
*/ | |
mutationFn: async ({ pk }) => deleteTaxonomy(pk), | |
onSettled: () => { | |
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] }); | |
}, | |
}); | |
}; | |
/** | |
* Builds the mutation to delete a taxonomy. | |
* @returns {(id: string) => void} - An object with the mutation configuration. | |
*/ | |
export const useDeleteTaxonomy = () => { | |
const queryClient = useQueryClient(); | |
const { mutate } = useMutation({ | |
mutationFn: async ({ pk }) => deleteTaxonomy(pk), | |
onSettled: () => { | |
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] }); | |
}, | |
}); | |
return (id) => mutate({ pk: 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.
Building on that, I would recommend that you return the mutate
function directly, so your code can look like this:
export const useDeleteTaxonomy = () => {
const queryClient = useQueryClient();
const { mutate } = useMutation({
/** @type {import("@tanstack/react-query").MutateFunction<any, any, {pk: number}>} */
mutationFn: async ({ pk }) => deleteTaxonomy(pk),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] });
},
});
return mutate;
};
const deleteTaxonomy = useDeleteTaxonomy();
//...
const onClickDeleteTaxonomy = React.useCallback(() => {
deleteTaxonomy({pk: taxonomy.id}, {
onSuccess: async() => {
setToastMessage(intl.formatMessage(taxonomyMessages.taxonomyDeleteToast, { name: taxonomy.name }));
navigate('/taxonomies');
},
onError: async() => {
// TODO: display the error to the user
},
});
}, [setToastMessage, navigate]);
Notice that this way you can directly access onSuccess
for that exact mutation, and you don't have to use useEffect
. Also notice that if you inspect these variables in your IDE, all the detailed type information is preserved and correct. For example, it knows what parameters the onSuccess
handler is given, including that the second parameter has the pk
of the object. Not that you need that here.
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 here
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 noticed some typing issues while driving by...
src/taxonomy/data/apiHooks.jsx
Outdated
@@ -26,6 +26,29 @@ const useTaxonomyListData = (org) => ( | |||
}) | |||
); | |||
|
|||
/** | |||
* Builds the mutation to delete a taxonomy. | |||
* @returns {Object} - An object with the mutation configuration. |
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.
* @returns {Object} - An object with the mutation configuration. | |
* @returns An object with the mutation configuration. |
You are actually erasing the useful type information here. Leave out the type and TypeScript will infer it automatically :)
After:
(assuming you also fixed the @ts-check
at the top of TaxonomyDetailPage.tsx
)
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.
Fixed here: 87d508c
src/taxonomy/data/apiHooks.jsx
Outdated
/** | ||
* Builds the mutation to delete a taxonomy. | ||
* @returns {Object} - An object with the mutation configuration. | ||
*/ | ||
export const useDeleteTaxonomy = () => { | ||
const queryClient = useQueryClient(); | ||
return useMutation({ | ||
/** | ||
* @type {import("@tanstack/react-query").MutateFunction< | ||
* any, | ||
* any, | ||
* { | ||
* pk: number | ||
* } | ||
* >} | ||
*/ | ||
mutationFn: async ({ pk }) => deleteTaxonomy(pk), | ||
onSettled: () => { | ||
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] }); | ||
}, | ||
}); | ||
}; | ||
|
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.
Building on that, I would recommend that you return the mutate
function directly, so your code can look like this:
export const useDeleteTaxonomy = () => {
const queryClient = useQueryClient();
const { mutate } = useMutation({
/** @type {import("@tanstack/react-query").MutateFunction<any, any, {pk: number}>} */
mutationFn: async ({ pk }) => deleteTaxonomy(pk),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['taxonomyList'] });
},
});
return mutate;
};
const deleteTaxonomy = useDeleteTaxonomy();
//...
const onClickDeleteTaxonomy = React.useCallback(() => {
deleteTaxonomy({pk: taxonomy.id}, {
onSuccess: async() => {
setToastMessage(intl.formatMessage(taxonomyMessages.taxonomyDeleteToast, { name: taxonomy.name }));
navigate('/taxonomies');
},
onError: async() => {
// TODO: display the error to the user
},
});
}, [setToastMessage, navigate]);
Notice that this way you can directly access onSuccess
for that exact mutation, and you don't have to use useEffect
. Also notice that if you inspect these variables in your IDE, all the detailed type information is preserved and correct. For example, it knows what parameters the onSuccess
handler is given, including that the second parameter has the pk
of the object. Not that you need that here.
src/taxonomy/data/types.mjs
Outdated
@@ -31,4 +32,10 @@ | |||
* @typedef {Object} UseQueryResult |
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.
You should not be defining or using UseQueryResult
here. Please delete this and replace all occurrences of import("./types.mjs").UseQueryResult
with import('@tanstack/react-query').UseQueryResult<T>
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.
Or, better yet - see the fixes I made for another PR in open-craft@01c9cd1 . Basically if you put the right type on the method in api.js
, then you can leave out the @returns
definition in apiHooks.tsx
and the type will still be correct.
open-craft@01c9cd1#diff-3aff02024d6cb28f96cce104799ab0f3bfe2e9559fe0cd572b9d745daeb590afL25-R25
open-craft@01c9cd1#diff-b5aca46925ac5f8adac9fbe39002822044019f623908e73f3dba1d5386048dcdL20
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.
Fixed here: 4bf6f0f
src/taxonomy/delete-dialog/index.jsx
Outdated
const handleInputChange = (event) => { | ||
if (event.target.value === deleteLabel) { | ||
setDeleteButtonDisabled(false); | ||
} else { | ||
setDeleteButtonDisabled(true); | ||
} | ||
}; | ||
|
||
const onClickDelete = () => { | ||
onClose(); | ||
onDelete(); | ||
}; |
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.
const handleInputChange = (event) => { | |
if (event.target.value === deleteLabel) { | |
setDeleteButtonDisabled(false); | |
} else { | |
setDeleteButtonDisabled(true); | |
} | |
}; | |
const onClickDelete = () => { | |
onClose(); | |
onDelete(); | |
}; | |
const handleInputChange = React.useCallback((event) => { | |
setDeleteButtonDisabled(event.target.value !== deleteLabel); | |
}); | |
const onClickDelete = React.useCallback(() => { | |
onClose(); | |
onDelete(); | |
}, [onClose, onDelete]); |
If you don't wrap these handlers in useCallback
, they change and become new functions every time, which will cause child components to re-render needlessly.
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 here: fd18b0f
+1 That's my preferred way to write these tests as well. It mocks the thing that's external (the API) while still properly testing all the integrated functionality of the UI - the queries, hooks, and react components. You can see https://github.com/openedx/frontend-app-course-authoring/blob/195c9e416c99bb8e53fc469d57887acf245a029c/src/taxonomy/tag-list/TagListTable.test.jsx which I wrote in that way. But if they're working, no need to change as we're getting tight on budget. Maybe just add a comment in the test that we could refactor them that way in the future. |
@ChrisChV Can you please address those nits, squash this down to one commit, then ping @xitij2000 to merge it for you? |
@xitij2000 It's ready for merge |
feat: Connect with delete API feat: Show toast after delete feat: Enable export for system taxonomies test: Adding tests feat: Delete submenu on details page style: Comment added style: Typos feat: Improve delete mutation style: Nits on types refactor: Taxonomy Card Menu and Taxonomy Details Menu refactor: Refactor delete function and tests style: Nit on list page refactor: Use localStorage to show toast feat: tagsCount configuration added chore: Nit on tests test: Add test to meet coverage refactor: Refactor Toast with Taxonomy context refactor: Fix typing nits and refactor delete mutation style: Nits on typing style: Nits nits
2e327e2
to
8690387
Compare
@ChrisChV 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
This reverts commit 1eff489.
Internal issue: https://2u-internal.atlassian.net/servicedesk/customer/portal/9/CR-6328?created=true Reverted 6 merged PRs due to problems. scroll was not working on editors potential problems with editor content loading ------------------------------------------------------ * Revert "fix(deps): update dependency @edx/frontend-lib-content-components to v1.177.4 (#742)" This reverts commit cc40e9d. * Revert "feat: add escalation email field for LTI-based proctoring providers (#736)" This reverts commit 0f483dc. * Revert "fix: video downloads (#728)" This reverts commit c5abd21. * Revert "fix: import api to chunk file (#734)" This reverts commit 6f7a992. * Revert "feat: Taxonomy delete dialog (#684)" This reverts commit 1eff489. * Revert "fix(deps): update dependency @edx/frontend-lib-content-components to v1.177.1 (#727)" This reverts commit dcabb77.
This adds:
Testing instructions
Delete
submenuActions
and click onDelete
submenuOther Info
TODO