Skip to content

Conversation

kathyavini
Copy link
Collaborator

@kathyavini kathyavini commented Oct 9, 2025

Description

This adds product deletion functionality to the inventory. The useRemoveProduct hook was based on the the squeaky clean useSaveProduct, 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 as onSaveProduct 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 on isAdmin status.

Jira link: https://lite-farm.atlassian.net/browse/LF-4966

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

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

  • Passes test case
  • UI components visually reviewed on desktop view
  • UI components visually reviewed on mobile view
  • Other (please explain)

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The precommit and linting ran successfully
  • I have added or updated language tags for text that's part of the UI
  • I have ordered translation keys alphabetically (optional: run pnpm i18n to help with this)
  • I have added the GNU General Public License to all new files

@kathyavini kathyavini self-assigned this Oct 9, 2025
@kathyavini kathyavini requested review from a team as code owners October 9, 2025 15:57
@kathyavini kathyavini added enhancement New feature or request new translations New translations to be sent to CrowdIn are present labels Oct 9, 2025
@kathyavini kathyavini requested review from Duncan-Brain and removed request for a team October 9, 2025 15:58
'FarmAddon',
'IrrigationPrescriptions',
'IrrigationPrescriptionDetails',
'SoilAmendmentProduct',
Copy link
Collaborator Author

@kathyavini kathyavini Oct 9, 2025

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!

Copy link
Collaborator

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={
Copy link
Collaborator Author

@kathyavini kathyavini Oct 9, 2025

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

@Duncan-Brain
Copy link
Collaborator

Duncan-Brain commented Oct 9, 2025

I think it is looking great!
No code problems here per se, but I did find one bug for the delete functionality as a whole.

Heres a long video bug:
https://github.com/user-attachments/assets/9fe6f503-9b56-4da5-96d0-d038439e9e26

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.

@kathyavini
Copy link
Collaborator Author

kathyavini commented Oct 9, 2025

@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:

  1. Delete a custom product and create a new one with the same name --> new product table record created ✅
  2. Try to delete while in active task --> prohibited ✅
  3. Complete task and delete --> product table record is non-deleted but product_farm table removed is set to true ✅

I think the video didn't show the product_farm table with the removed boolean, but you actually kind of see it in the inventory view when the text goes to grey -- this is the styling for when a row has the removed property (I actually didn't set that up when I made this table... I think it's a holdover from removed animal styling 😂) Ultimately the removed products will be hidden from inventory, but that is covered in the adjust selectors ticket that Sayaka will be picking up next.

So it looks pretty good to me; I'm happy to see it behaving as we had planned!

Duncan-Brain
Duncan-Brain previously approved these changes Oct 10, 2025
Copy link
Collaborator

@Duncan-Brain Duncan-Brain left a 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.

Copy link
Collaborator

@SayakaOno SayakaOno left a 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',
Copy link
Collaborator

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) => {
Copy link
Collaborator

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!)

Copy link
Collaborator Author

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!

Copy link
Collaborator

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 :))

Copy link
Collaborator

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!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a thread where a Redux and Reselect maintainer explained this, and the official Redux doc was also very helpful. I think these will be great guidance to follow going forward!

Copy link
Collaborator Author

@kathyavini kathyavini Oct 14, 2025

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.

Copy link
Collaborator

@Duncan-Brain Duncan-Brain Oct 15, 2025

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!

Copy link
Collaborator

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!

@kathyavini
Copy link
Collaborator Author

kathyavini commented Oct 10, 2025

@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!

SayakaOno
SayakaOno previously approved these changes Oct 10, 2025
Copy link
Collaborator

@SayakaOno SayakaOno left a 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) => {
Copy link
Collaborator

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 :))

@kathyavini
Copy link
Collaborator Author

kathyavini commented Oct 10, 2025

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 FormMode.READ_ONLY was missing in that logical branch 🙇

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).

Is ChatGPT for sure correct about this????! For some reason I can't believe it... I can't imagine that every useSelector() call was meant to be wrapped in useMemo()! I really am clueless though so I'm going to have to hit the RTK docs and read into this now as well 🙏

Copy link
Collaborator

@Duncan-Brain Duncan-Brain left a 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!

@SayakaOno SayakaOno added this pull request to the merge queue Oct 15, 2025
Merged via the queue into integration with commit a107a35 Oct 15, 2025
5 checks passed
@SayakaOno SayakaOno deleted the LF-4966-implement-delete-product-functionality branch October 15, 2025 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request new translations New translations to be sent to CrowdIn are present

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants