Fix plant placement + add a Seed Tray (#39) (#42)
Build image / build-and-push (push) Successful in 21s

Use the full Plant the picker returns (fixes the silent placement failure) and add a per-garden Seed Tray for quick repeat placing. Review fixes: disarm on tray-remove, cap load, consolidate toolbar guards, rename interface, tidy.

Closes #39.

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #42.
This commit is contained in:
2026-07-19 06:09:51 +00:00
committed by steve
parent fef3f601bf
commit 5cdc2779d7
4 changed files with 226 additions and 25 deletions
+56 -24
View File
@@ -7,6 +7,7 @@ import { Inspector } from '@/editor/Inspector'
import { PlopInspector } from '@/editor/PlopInspector'
import { PlantPicker } from '@/editor/PlantPicker'
import { Palette } from '@/editor/Palette'
import { SeedTray } from '@/editor/SeedTray'
import { ClearBedModal } from '@/editor/ClearBedModal'
import { EditorHint } from '@/editor/EditorHint'
import { objectDisplayName } from '@/editor/kinds'
@@ -14,8 +15,10 @@ import { useEditorStore } from '@/editor/store'
import type { EditorGarden } from '@/editor/types'
import { ShareGardenModal } from '@/components/gardens/ShareGardenModal'
import { useMe } from '@/lib/auth'
import { toEditorObject, useGardenFull, useUpdateObject, useUpdatePlanting } from '@/lib/objects'
import { toEditorObject, useEnsurePlantInFull, useGardenFull, useUpdateObject, useUpdatePlanting } from '@/lib/objects'
import { toEditorPlanting } from '@/lib/plantings'
import type { Plant } from '@/lib/plants'
import { useSeedTray } from '@/lib/seedTray'
import { usePageTitle } from '@/lib/usePageTitle'
const routeApi = getRouteApi('/gardens/$gardenId')
@@ -43,6 +46,8 @@ export function GardenEditorPage() {
const updatePlanting = useUpdatePlanting(gid)
const updateObject = useUpdateObject(gid)
const ensurePlant = useEnsurePlantInFull(gid)
const { trayPlants, add: addToTray, remove: removeFromTray } = useSeedTray(gid)
// Which plant-picker flow is open: 'place' arms a plant for repeat placement;
// 'change' swaps the selected plop's plant.
@@ -231,10 +236,30 @@ export function GardenEditorPage() {
const selectedPlop = plantings.find((p) => p.id === selectedPlantingId) ?? null
const focusedPlops = focusedObjectId != null ? plantings.filter((p) => p.objectId === focusedObjectId) : []
function onPickPlant(plantId: number) {
// Arm a plant for tap-to-place. The full catalog carries plants not yet in this
// garden's /full payload, so make sure the chosen one is in the cache first —
// otherwise its placed plops render without an icon/color until a refetch.
function armPlant(plant: Plant) {
ensurePlant(plant)
setArmedPlant(plant)
}
// Removing the armed plant from the tray also disarms it, so placement doesn't
// silently continue for a plant the user just cleared out.
function removeFromTrayAndDisarm(id: number) {
removeFromTray(id)
if (armedPlant?.id === id) setArmedPlant(null)
}
// The picker hands back the full Plant (from the whole catalog), so use it
// directly rather than re-resolving against the garden's referenced-plant map —
// a not-yet-placed plant isn't in that map, which used to silently abort the
// pick (nothing armed, picker left open). Picking (place OR change) makes sure
// the plant renders and drops it into this garden's tray for quick reuse.
function onPickPlant(plant: Plant) {
if (!canEdit) return // defense in depth: viewers can't reach the picker anyway
const plant = plantsById.get(plantId)
if (!plant) return
ensurePlant(plant)
addToTray(plant)
if (picker === 'change' && selectedPlop) {
updatePlanting.mutate({ id: selectedPlop.id, version: selectedPlop.version, plantId: plant.id })
} else {
@@ -268,26 +293,33 @@ export function GardenEditorPage() {
</button>
<span className="max-w-[8rem] truncate text-muted">{objectDisplayName(focusedObject)}</span>
{canEdit &&
(!focusedObject.plantable ? (
<span className="text-xs text-muted">Not plantable</span>
) : armedPlant ? (
<Button variant="ghost" className="px-2 py-1 text-xs" onClick={() => setArmedPlant(null)}>
Placing {armedPlant.icon} Done
</Button>
(focusedObject.plantable ? (
<>
<SeedTray
trayPlants={trayPlants}
armedPlantId={armedPlant?.id ?? null}
onArm={armPlant}
onRemove={removeFromTrayAndDisarm}
onOpenPicker={() => setPicker('place')}
/>
{armedPlant && (
<Button variant="ghost" className="px-2 py-1 text-xs" onClick={() => setArmedPlant(null)}>
Done
</Button>
)}
{focusedPlops.length > 0 && (
<Button
variant="ghost"
className="px-2 py-1 text-xs text-red-600 dark:text-red-400"
onClick={() => setClearing(true)}
>
Clear ({focusedPlops.length})
</Button>
)}
</>
) : (
<Button className="px-2 py-1 text-xs" onClick={() => setPicker('place')}>
+ Add plant
</Button>
<span className="text-xs text-muted">Not plantable</span>
))}
{canEdit && focusedObject.plantable && focusedPlops.length > 0 && (
<Button
variant="ghost"
className="px-2 py-1 text-xs text-red-600 dark:text-red-400"
onClick={() => setClearing(true)}
>
Clear ({focusedPlops.length})
</Button>
)}
</div>
)}
<GardenCanvas garden={garden} objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />
@@ -297,7 +329,7 @@ export function GardenEditorPage() {
<EditorHint>Pick a shape from the palette, then tap the field to place your first bed.</EditorHint>
)}
{canEdit && focusedObject?.plantable && focusedPlops.length === 0 && !armedPlant && (
<EditorHint position="top">Tap + Add plant, then tap inside the bed to place plops.</EditorHint>
<EditorHint position="top">Pick a plant from the tray, then tap inside the bed to place it.</EditorHint>
)}
</div>
@@ -335,7 +367,7 @@ export function GardenEditorPage() {
<PlantPicker
unit={garden.unitPref}
onClose={() => setPicker(null)}
onSelect={(p) => onPickPlant(p.id)}
onSelect={onPickPlant}
/>
)}