From eb18622b78677754cac5583674a330a50595d403 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Mon, 21 Sep 2026 16:05:12 +1200 Subject: [PATCH] Make the crop chip the planting's menu, for annuals and perennials Clicking a chip opens the planting's menu under it (view, edit, add photo, record harvest, save seeds, mark as finished), in place of the three dots on annual rows, so perennials get the same menu. A caret shows the chip opens something. Someone who can't change the planting still gets a plain link chip. The menu items are lowercase for every planting menu. Co-Authored-By: Claude Sonnet 5 --- app/assets/stylesheets/_garden_cards.scss | 37 +++++++++++++++------- app/javascript/components/ActionsMenu.jsx | 4 +-- app/javascript/components/GardenCard.jsx | 22 +++++++------ app/javascript/components/PlantingChip.jsx | 33 +++++++++++++++++++ app/javascript/components/PlantingRow.jsx | 33 +++++-------------- app/javascript/plantingActions.js | 15 +++++++++ app/serializers/garden_card_serializer.rb | 6 +++- 7 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 app/javascript/components/PlantingChip.jsx create mode 100644 app/javascript/plantingActions.js diff --git a/app/assets/stylesheets/_garden_cards.scss b/app/assets/stylesheets/_garden_cards.scss index 163432552..097bc60e0 100644 --- a/app/assets/stylesheets/_garden_cards.scss +++ b/app/assets/stylesheets/_garden_cards.scss @@ -80,7 +80,7 @@ $garden-card-line: #e6e6e6; // One annual planting: crop | how it's getting on | actions, in aligned columns. .planting-row { display: grid; - grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr) 2.5rem; + grid-template-columns: minmax(11rem, 15rem) minmax(0, 1fr); align-items: center; padding: 0.6rem 0.5rem; border-top: 1px solid $garden-card-line; @@ -192,21 +192,17 @@ $garden-card-line: #e6e6e6; color: $garden-card-muted; font-size: 0.8rem; } +} - .planting-row-actions { - text-align: right; - } +// The planting menu's items are lowercase. Done in CSS because the labels are +// the shared button translations, which other pages show in sentence case. +.planting-menu .dropdown-item { + text-transform: lowercase; } // The "more" button: three dots in a small round button. The theme gives every // .btn a 5.5rem minimum width, which made the hover background a wide box, and // adds a dropdown caret we don't want. -// The planting menu's items are lowercase. Done in CSS because the labels are -// the shared button translations, which other pages show in sentence case. -.planting-row .dropdown-item { - text-transform: lowercase; -} - .actions-toggle-dots { display: inline-flex; align-items: center; @@ -263,7 +259,7 @@ $garden-card-line: #e6e6e6; } .planting-row { - grid-template-columns: minmax(0, 1fr) 2.5rem; + grid-template-columns: minmax(0, 1fr); .planting-row-status { grid-column: 1 / -1; @@ -315,3 +311,22 @@ $garden-card-line: #e6e6e6; font-size: 0.9rem; } } + +// A crop chip that is also the planting's menu: a caret says it opens something, +// and it takes a slightly deeper tint while pointed at or open. +.chip-menu-toggle { + cursor: pointer; + text-decoration: none; + + &::after { + margin-left: 0.5rem; + vertical-align: 0.15em; + } + + &:hover, + &:focus-visible, + &.show { + background-color: var(--bs-success-border-subtle); + color: var(--bs-success-text-emphasis); + } +} diff --git a/app/javascript/components/ActionsMenu.jsx b/app/javascript/components/ActionsMenu.jsx index e35a99c13..396045a8f 100644 --- a/app/javascript/components/ActionsMenu.jsx +++ b/app/javascript/components/ActionsMenu.jsx @@ -16,7 +16,7 @@ function handled(event, buttonId) { if (dropdown) dropdown.hide(); } -export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', onSelect}) { +export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, className = 'btn dropdown-toggle', menuClassName = 'dropdown-menu-end', onSelect}) { if (!actions || actions.length === 0) return null; const buttonId = `actions-${id}`; @@ -35,7 +35,7 @@ export default function ActionsMenu({id, actions, label = 'Actions', ariaLabel, > {label} -
+
{actions.map((action) => ( {action.divider &&
} diff --git a/app/javascript/components/GardenCard.jsx b/app/javascript/components/GardenCard.jsx index 7f0400a4e..346cb800c 100644 --- a/app/javascript/components/GardenCard.jsx +++ b/app/javascript/components/GardenCard.jsx @@ -2,7 +2,7 @@ import React from 'react'; import {isPlainClick} from '../events'; import ActionsMenu from './ActionsMenu'; -import CropChip from './CropChip'; +import PlantingChip from './PlantingChip'; import PlantingRow from './PlantingRow'; // One garden: its name and actions menu (top right), a picture, then what is @@ -11,6 +11,14 @@ import PlantingRow from './PlantingRow'; export default function GardenCard({garden, defaultIconUrl, onPlant, highlightedId, highlightKind, onPlantingUpdated, onEditPlanting, onHarvestPlanting, onSaveSeedsPlanting, onAddPhoto, onFinishPlanting}) { const {id, name, url, image_url: imageUrl, owner, actions, plant_url: plantUrl, perennials, annuals} = garden; const empty = perennials.length === 0 && annuals.length === 0; + // What each planting's menu items open, for annuals and perennials alike. + const handlers = { + edit: onEditPlanting, + harvest: onHarvestPlanting, + seeds: onSaveSeedsPlanting, + finish: onFinishPlanting, + photo: onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href})), + }; function plant(event) { if (onPlant && isPlainClick(event)) { event.preventDefault(); @@ -59,12 +67,12 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted {perennials.length > 0 ? (
{perennials.map((planting) => ( - ))}
@@ -84,11 +92,7 @@ export default function GardenCard({garden, defaultIconUrl, onPlant, highlighted highlighted={planting.id === highlightedId} highlightKind={highlightKind} onUpdated={onPlantingUpdated} - onEdit={onEditPlanting} - onHarvest={onHarvestPlanting} - onSaveSeeds={onSaveSeedsPlanting} - onFinish={onFinishPlanting} - onAddPhoto={onAddPhoto && ((planting, action) => onAddPhoto({label: planting.crop.name, href: action.href}))} + handlers={handlers} /> ))}
diff --git a/app/javascript/components/PlantingChip.jsx b/app/javascript/components/PlantingChip.jsx new file mode 100644 index 000000000..f9fec087e --- /dev/null +++ b/app/javascript/components/PlantingChip.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import {selectPlantingAction} from '../plantingActions'; +import ActionsMenu from './ActionsMenu'; +import CropChip from './CropChip'; + +// A planting's crop chip, which is also its menu: click it for view, edit, +// harvest and the rest, the same for annuals and perennials. Someone who can't +// change the planting has no menu, so for them it stays a plain link to it. +export default function PlantingChip({planting, defaultIconUrl, highlighted, handlers}) { + const {id, url, crop, actions} = planting; + + if (!actions || actions.length === 0) { + return ; + } + + return ( + + + {crop.name} + + )} + ariaLabel={`Actions for ${crop.name}`} + className={`chip crop-chip chip-menu-toggle dropdown-toggle${highlighted ? ' planting-just-added' : ''}`} + menuClassName="dropdown-menu-start planting-menu" + onSelect={selectPlantingAction(planting, handlers)} + /> + ); +} diff --git a/app/javascript/components/PlantingRow.jsx b/app/javascript/components/PlantingRow.jsx index fe900a14d..b0798c3b5 100644 --- a/app/javascript/components/PlantingRow.jsx +++ b/app/javascript/components/PlantingRow.jsx @@ -1,22 +1,21 @@ import React from 'react'; import {formatDate} from '../dates'; -import {isPlainClick} from '../events'; -import ActionsMenu from './ActionsMenu'; -import CropChip from './CropChip'; import PlantedDate from './PlantedDate'; +import PlantingChip from './PlantingChip'; -// One annual planting as a row of three aligned columns: the crop (and when it -// was planted), how it is getting on (badges, then a progress bar or a note -// when there is nothing to predict from), and its own actions menu. -export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, onEdit, onHarvest, onSaveSeeds, onAddPhoto, onFinish}) { - const {id, url, crop, badges} = planting; +// One annual planting as a row of two aligned columns: the crop, which is also +// the planting's menu (and when it was planted), and how it is getting on +// (badges, then a progress bar or a note when there is nothing to predict from). +// `handlers` is what the menu's items open (see selectPlantingAction). +export default function PlantingRow({planting, defaultIconUrl, highlighted, highlightKind = 'added', onUpdated, handlers}) { + const {badges} = planting; return (
- + {highlighted && highlightKind === 'harvested' && (