feat(MasterMix): distinguish per reaction ingredients and add recipe mode - #330
Merged
Conversation
… mix Ingredients like template or standard are added to each reaction individually. They belong to the reaction volume, but must never be multiplied by the number of Ansätze or included in the master mix sum. Callers pass them via the new optional `perReactionIngredients` prop. They render below the Gesamtvolumen row, without a pipetting loss column value, followed by a Reaktionsvolumen row. The exported `reactionVolume` saves callers from summing both lists themselves, e.g. to calculate molarities. Pagination is disabled because a summary row paginated onto the second page is misleading. No caller currently exceeds the previous page size of 10 rows, so nothing changes visually today. 🤖 Generated with Claude Code
The flat table did not convey that the master mix is part of the reaction mix, and the title read "<name> MasterMix" even with per reaction ingredients present. - render the master mix in its own card inside the reaction mix card - name the mix after what it actually is: Reaktionsmix or MasterMix - move the per reaction rows into a table without onRow, so Table stops injecting the pointer cursor on rows that are not clickable - split the component into one file per component 🤖 Generated with Claude Code
One table replaces the two that had to be aligned by hand, which removes the need for fixed column widths and lets every volume be right aligned. The master mix now appears as a labelled block inside the reaction mix, closed off by its total, so what follows reads as added per reaction. Ingredients are indented to reserve room for a check mark that marks them as pipetted without shifting the layout. `mode="recipe"` shows the mix unscaled, for contexts that have no run yet and therefore no meaningful number of reactions. Visible for existing callers: the card hugs the table instead of stretching across the surrounding layout, ingredients carry the indent for the check mark, and volumes for a single reaction are dimmed where a scaled column shows the amount to pipette. 🤖 Generated with Claude Code
Both ingredient lists are keyed by the consumer and existing callers number each list from 1, so passing the same key in both collided into one React row key. React then warns about duplicate children and remounts the affected row on every re-render, e.g. on each pipetted toggle. 🤖 Generated with Claude Code
The default branch would have multiplied any row kind added later by the number of reactions plus pipetting loss, showing a wrong volume on a work list without a compile error. 🤖 Generated with Claude Code
The left edge used dividerColor, which is gray4 and therefore invisible against the gray3 background of the total row, so the box looked open on its left side exactly where the closing line is darkest. 🤖 Generated with Claude Code
Only used by the component that renders it. 🤖 Generated with Claude Code
Hugging the content made the card width depend on the ingredient names, so cards placed side by side in a grid no longer lined up. Capping the table keeps it from running wide without the card claiming a width of its own. 🤖 Generated with Claude Code
Without per reaction ingredients there is no section row to indent against, so the ingredients sat 20px right of their own total. Indenting every row the master mix consists of also delimits the block well enough that the gap before the per reaction rows became unnecessary. The predicate for those rows already existed for scaling, so both the indent and the scaling now derive from one definition of what the master mix is. 🤖 Generated with Claude Code
The separate label row said what the block was, while the row below it said only Gesamtvolumen — yet that volume is what goes into each reaction, so it is the line item the label belongs on. Naming it there drops a row, puts the master mix on the same level as the other per reaction rows, and leaves every row with a volume, which collapses the row union back into one shape. 🤖 Generated with Claude Code
Indenting the master mix total kept its own ingredients aligned with it, but broke the column the pipettor actually adds up: master mix plus template equals the reaction volume. Outdented totals also follow the convention any invoice or recipe uses, and the indent no longer depends on whether the mix is nested. 🤖 Generated with Claude Code
Indenting only the master mix ingredients could not be derived from either reading of what an indent means here, so the master mix total and the template ended up on different levels although both are added per reaction. One step per sum puts them side by side below the reaction volume, and makes the nesting itself carry what the box previously had to. 🤖 Generated with Claude Code
The left edge bracketed the master mix from outside the block it enclosed, which the indentation already expresses.
A caller without types that passes no pipetting loss now falls back to the recipe view instead of dereferencing it.
Contributor
Author
|
Self-reviewed at 306bcb1 🤖 Posted by Claude Code |
…reaction The predicate appeared verbatim at four sites in three files, each time in a different disguise. Naming it also names what "nested" left implicit.
Both return branches spelled out the same prefix.
Both arms of the union are new and unreleased, and MasterMixProps is enough to annotate a call site. Removing them after the release would be breaking.
The cursor was decided for the whole table by calling onRow with an empty object, which lies to a callback that is typed for a record and only worked because no caller dereferenced it. Deriving it from the row props each row actually returns drops the workaround MasterMix needed to opt single rows out.
Both rules were only reachable through a rendered table, so nothing would turn red if the nesting were flattened or the row keys collided again.
The closing line exists exactly when rows follow the total, which the selector can say by itself. Deciding it in the row class instead required threading the nesting through rowClassName, and made the two total rows differ where they do not.
Table defaults to "id", so the field has to be named, but not through a closure.
simbig
marked this pull request as ready for review
July 30, 2026 19:12
mic-web
requested changes
Jul 31, 2026
Co-authored-by: mic-web <4804412+mic-web@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates MasterMix to correctly separate ingredients that are pipetted per well from the shared master mix, and introduces a “recipe” (unscaled) mode so mixes can be displayed without run-specific scaling inputs.
Changes:
- Add
perReactionIngredientssupport and compute a separate reaction total volume (master mix + per-reaction additions). - Introduce
mode="recipe"via a discriminated union inMasterMixProps, and adjust rendering/interaction accordingly. - Refactor the UI into a dedicated
MixTablewith indentation, row styling, and pipetting-loss behavior limited to master-mix rows; update genericTablecursor behavior.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Table/index.tsx | Derives per-row pointer cursor from that row’s onClick instead of a global hover rule. |
| src/MasterMix/VolumeTable.tsx | Adds shared styling/constants for total, pipetted, and reference-volume rows. |
| src/MasterMix/volumeColumns.tsx | Introduces column rendering for names, per-reaction volumes, and optional scaled volumes with PV. |
| src/MasterMix/types.ts | Adds ReactionMix, perReactionIngredients, recipe/scaled union props, and MasterMixTableRow. |
| src/MasterMix/reactionVolume.ts | Adds reactionVolume (and sumVolume) that includes per-reaction ingredients. |
| src/MasterMix/reactionVolume.test.ts | Adds unit tests for reactionVolume. |
| src/MasterMix/pipettingLossTableColumn.tsx | Applies PV only to master-mix rows; renders “–” for per-reaction and reaction-total rows. |
| src/MasterMix/pipettingLossTableColumn.test.tsx | Updates tests to the new row model and PV rendering rules. |
| src/MasterMix/MixTable.tsx | New table component that assembles rows, applies classes, and handles “pipetted” checkoff clicks. |
| src/MasterMix/mixRows.ts | Builds hierarchical rows (ingredients → totals → per-reaction additions → reaction total). |
| src/MasterMix/mixRows.test.ts | Adds tests for row ordering, totals, and key namespacing. |
| src/MasterMix/MasterMixRowName.tsx | New indented row-name renderer with an inline pipetted check mark. |
| src/MasterMix/MasterMix.test.tsx | Updates/expands integration tests for reaction-mix rendering, PV exclusion, and recipe mode behavior. |
| src/MasterMix/index.tsx | Refactors MasterMix to use MixTable, support reaction mixes, and export reactionVolume. |
| src/MasterMix/index.stories.tsx | Adjusts stories for new props and adds per-reaction and recipe examples. |
| src/MasterMix/indentLevel.ts | Computes indentation levels for the hierarchical table layout. |
| src/MasterMix/indentLevel.test.ts | Adds tests for indentation behavior across row kinds. |
| src/MasterMix/hasPerReactionIngredients.ts | Adds a type-guard helper to detect per-reaction ingredients. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 3, 2026
# [20.5.0](v20.4.1...v20.5.0) (2026-08-03) ### Features * **MasterMix:** distinguish per reaction ingredients and add recipe mode ([#330](#330)) ([0817ab1](0817ab1))
|
🎉 This PR is included in version 20.5.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
MasterMixscaled every ingredient bycountplus pipetting loss, which is wrong for anything pipetted per well — template, standard, filler water. In limes-frontend (AE-3800) that asked for 110 µl cDNA in the shared mix and reported 440 µl instead of 330 µl.countandpipettingLosswere also required, though a recipe has no run yet.Approach
perReactionIngredients— outside the master mix and its sum, no pipetting loss, followed by the resultingReaktionsvolumen.mode="recipe"— a union with the scaled mode, so neither can be half-specified.reactionVolume(mix)export. A callback cannot work: the molarity goes intotitle, which is input toMasterMix.TableThe pointer cursor came from probing
onRow({})with a fabricated record behind a@ts-expect-errorand then covered every row. It is now derived per row from that row's ownonClick, since only ingredients can be checked off here, not the totals.Compatibility
Existing limes-frontend and nemo callers pass
name,count,ingredientsandpipettingLoss, which still matches.MasterMixPropsis narrower —countandpipettingLosslive on the scaled arm of the union only — but no consumer imports that type, so this ships as a minor.🤖 Generated with Claude Code