-
Notifications
You must be signed in to change notification settings - Fork 94
LF-4966 implement delete product functionality #3896
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
LF-4966 implement delete product functionality #3896
Conversation
'FarmAddon', | ||
'IrrigationPrescriptions', | ||
'IrrigationPrescriptionDetails', | ||
'SoilAmendmentProduct', |
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 tag was added at the same time as the edit and add mutations, but since it was never provided (the GET products is of course a Saga), it was never used for anything.
I'm not sure if it was a placeholder, but for now I've removed it so it doesn't seem like something that can be meaningfully invalidated (it can't; since it's not provided!) Please let me know if I've missed something 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 don't know why I did that, thank you for cleaning it up! On an unrelated note, I regret naming products “soil amendment products” everywhere 😞
</Drawer> | ||
<> | ||
<Drawer | ||
isOpen={ |
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.
As always wrapping the return in a parent fragment has annoyingly made the whole return into a diff, but the actual changes within <Drawer />
are the two extra conditionals into isOpen
, and the isAdmin
argument into renderDrawerTitle
I think it is looking great! Heres a long video bug: Essentially I create two inventory with the same name, the second deletion looks like it sticks, but not actually in the database. Purging state by logout doesn't help. I think at its core we are using the deleted prop in db as both "archived" and deleted and I think most of the app uses deleted a specific way. Its definitely possible to continue trying to differentiate this way here on the frontend.. but maybe it would be easier to differentiate removed from deleted like animals and others. |
@Duncan-Brain I love your video, it is so complete!! 😍 I got worried when you said bug, but I'm super relieved to see it's actually behaving 100% as designed! There is a flowchart here that would have been impossible to find on Jira, sorry (different ticket and collapsed by default), but you actually stepped through all the steps for a custom product in this one video:
I think the video didn't show the So it looks pretty good to me; I'm happy to see it behaving as we had planned! |
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 my mistake! Thanks for the link to the flow chart. And so removing the deleted products from the react select options, that caused the task not to create will be on that other ticket then -- very good!
Cool approving but you wanted Sayakas approval here so you two can merge.
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 great!
As for the state, isRemoveModalOpen
and isCannotRemoveModalOpen
could be combined (eg. modalType?), but I think it's good as is!
I'm most interested in the selector decision I mentioned in the inline comment 🙂
'FarmAddon', | ||
'IrrigationPrescriptions', | ||
'IrrigationPrescriptionDetails', | ||
'SoilAmendmentProduct', |
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 don't know why I did that, thank you for cleaning it up! On an unrelated note, I regret naming products “soil amendment products” everywhere 😞
}); | ||
}; | ||
|
||
export const isProductUsedInPlannedTasksSelector = (product_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 don't think I've seen this kind of selector before, what made you go with a selector instead of a function or hook? (A selector wouldn’t have been my first choice, so I’m just curious to learn different approaches!)
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.
Huh! That's interesting. I immediately thought of a selector here. It probably is because I just wrote this query on the backend, and (is this even reasonable??!) in my mind the Redux selector is the closest frontend approximation to a model query?
I do like getting the memoization automatically by putting it in a selector, but I'm definitely not married to it if you think a function would be better!
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've been paying more attention to selector memoization recently (since you asked about shallowEqual 😂), so I'm still learning, and I'll borrow ChatGPT's explanation...
if we call useSelector(isProductUsedInPlannedTasksSelector(product_id)) directly in the render, a new selector is created every time, so
createSelector
won’t actually memoize across renders. It works perfectly if the selector instance is stable (e.g., with useMemo).
So, to benefit from the selector’s memoization, we’d need useMemo; without it, I think it works just like a plain function! (but we've been doing it, and I'm fine with the selector without useMemo :))
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'd be interested too if you have some articles that helped you understand!
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 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.
Ahhhh I'm not sure I find that maintainers' comments very easy to understand 😓 As as the original poster said, the selector factory pattern is right from those very docs you linked so it's a bit rough that they followed the docs and got a "you're using createSelector
wrong by creating a factory function"!
Well if it's truly an anti-pattern, it's kind of horrifying because we have so many selector factories in app. I think I'm still fuzzy on what would be the correct time to use one -- do you have good sense @SayakaOno?
Interestingly, removing the factory pattern here did cause a crash on the import of the task selector into the product slice, and made me realize this selector shouldn't have been in product
in the first place... that's a plus I think! 👍
I did completely lose my desire to see this as a selector in the process of looking into this, but I kept it as such only because I wanted to see the memoization working for my own understanding.
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 for the articles @SayakaOno ! I have not thought this hard about this stuff before now!
So how I am understanding the guidance is:
Use a selector function for simple lookups on state:
// slice
const selectC = state => state.c
// app
const useC = useSelector(selectC)
It seems like createSelector
is itself a essentially a useMemo wrapping a selector, it has input args that it watches for changes for and the output function using those args so for "expensive" logic use createSelector
:
// slice
const selectCPrime = createSelector(
(selectC), (C) => {return C.map(C*100000).filter().etc();}
)
// app
const useC = useSelector(CPrime);
Create a factory pattern if we plan on purposefully creating many unique memoized functions. For example a function with a state that should not be shared (in my head an example is state.isLoading on a bunch of separate items -- but its not a great example).
//slice
const selectCPrimeFactory = () =>
createSelector((selectC, isLoading), (C, isLoading) => {return isLoading || C.map(C*100000).filter().etc();}) >
// app
const item1 = useSelector(selectCPrimeFactory()(isLoading1))
const item2 = useSelector(selectCPrimeFactory()(isLoading2))
Using a hook:
It seemed like we were using hooks when we have RTKquery set up, so that was why it seemed right to me to use a selector. In the future if we modularize our rtkQuery files (similar to Gursimrans weather API) using the reducerPath
arg in createAPI()
we could store and populate some slices so that we have more integration with core toolkit where valuable as well as access to createSelectors for memoization without creating as many custom hooks? I added an AI convo in Tech Daily note with some examples I thought looked good.
So for this function! Haha I think you did the right thing refactoring and it looks correct!
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.
Duncan's interpretation is aligned with mine. I created a quick demo to see how each option behaves in practice, so we could take a look at it on Monday if you guys haven’t tested it yet. I couldn’t think of good examples where a selector factory would be preferable either, but the products selector might be a potential case. I’ll talk about it on Monday as well!
I’ll also try to familiarize myself with RTK Query’s reducerPath by then. A lot to learn...
The updated selector looks great, thank you Joyce!
packages/webapp/src/containers/ProductInventory/ProductForm/useRemoveProduct.ts
Outdated
Show resolved
Hide resolved
packages/webapp/src/containers/ProductInventory/ProductForm/useRemoveProduct.ts
Outdated
Show resolved
Hide resolved
@SayakaOno I would like to try the modal state refactor! 🙏 Just wanted to check -- you mean something like this? export enum ModalType {
NONE = 'none',
CONFIRM = 'confirm',
CANNOT_REMOVE = 'cannotRemove',
}
const [modalType, setModalType] = useState<ModalType>(ModalType.NONE) Edit: I'll push a change like this but if you could please double-check! |
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 modalType
could be undefined
instead of none
, but it's probably a preference?
Looks good! ❤️
}); | ||
}; | ||
|
||
export const isProductUsedInPlannedTasksSelector = (product_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've been paying more attention to selector memoization recently (since you asked about shallowEqual 😂), so I'm still learning, and I'll borrow ChatGPT's explanation...
if we call useSelector(isProductUsedInPlannedTasksSelector(product_id)) directly in the render, a new selector is created every time, so
createSelector
won’t actually memoize across renders. It works perfectly if the selector instance is stable (e.g., with useMemo).
So, to benefit from the selector’s memoization, we’d need useMemo; without it, I think it works just like a plain function! (but we've been doing it, and I'm fine with the selector without useMemo :))
Sorry to invalidate review @SayakaOno but I just spotted a bug! If the API error snackbar triggers the form mode re-opens but without the buttons since the reset to
Is ChatGPT for sure correct about this????! For some reason I can't believe it... I can't imagine that every |
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.
Still working for me!
Description
This adds product deletion functionality to the inventory. The
useRemoveProduct
hook was based on the the squeaky cleanuseSaveProduct
, but with a need for a lot more state in this flow, I don't know if I can get it much cleaner... certainly no where near as tidy asonSaveProduct
right now! @SayakaOno if you don't mind checking that I did not destroy your vision for<ProductForm />
here 🙏We had talked briefly in tech daily on Wed Oct 8 about supplying a "Removing..." title to the drawer while the remove modal was open, but when I looked again at Figma I realized that the drawer is closed while the modal is open, so I went with that rather than adding a title.
This PR also moves the menu item for "Inventory" out of the
isAdmin
code block, restoring it to workers (sorry! 🙇) and hides the delete button conditionally based onisAdmin
status.Jira link: https://lite-farm.atlassian.net/browse/LF-4966
Type of change
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
Checklist:
pnpm i18n
to help with this)