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' && (
Harvest recorded
)}
@@ -39,22 +38,6 @@ export default function PlantingRow({planting, defaultIconUrl, highlighted, high
)}
-
-
}
- ariaLabel={`Actions for ${crop.name}`}
- className="btn btn-sm btn-link actions-toggle-dots"
- onSelect={(action, event) => {
- const open = {edit: onEdit, harvest: onHarvest, seeds: onSaveSeeds, photo: onAddPhoto, finish: onFinish}[action.key];
- if (open && isPlainClick(event)) {
- event.preventDefault();
- open(planting, action);
- }
- }}
- />
-
);
}
diff --git a/app/javascript/plantingActions.js b/app/javascript/plantingActions.js
new file mode 100644
index 000000000..6bbdb2111
--- /dev/null
+++ b/app/javascript/plantingActions.js
@@ -0,0 +1,15 @@
+import {isPlainClick} from './events';
+
+// What a click on one of a planting's menu items does: if there is a dialog for
+// that item (handlers is keyed by the action's key: edit, harvest, seeds, photo,
+// finish) it opens it, over the list; anything else, and ctrl/cmd-click, follows
+// the link as usual.
+export function selectPlantingAction(planting, handlers) {
+ return (action, event) => {
+ const open = handlers[action.key];
+ if (open && isPlainClick(event)) {
+ event.preventDefault();
+ open(planting, action);
+ }
+ };
+}
diff --git a/app/serializers/garden_card_serializer.rb b/app/serializers/garden_card_serializer.rb
index 66c7d9123..dd1741eb9 100644
--- a/app/serializers/garden_card_serializer.rb
+++ b/app/serializers/garden_card_serializer.rb
@@ -119,7 +119,11 @@ class GardenCardSerializer
end
def perennial(planting)
- { id: planting.id, url: routes.planting_path(planting), crop: crop_chip(planting) }
+ {
+ id: planting.id, url: routes.planting_path(planting), crop: crop_chip(planting),
+ planted_at: planting.planted_at, today: Time.zone.today, can_edit: can?(:edit, planting),
+ actions: planting_actions(planting)
+ }
end
def annual(planting)