diff --git a/web/src/editor/JournalPanel.tsx b/web/src/editor/JournalPanel.tsx
index fba7d63..5b6ec4d 100644
--- a/web/src/editor/JournalPanel.tsx
+++ b/web/src/editor/JournalPanel.tsx
@@ -37,6 +37,8 @@ export function JournalPanel({
objects,
scopeObjectId,
onScopeChange,
+ scopePlantingId = null,
+ onScopePlantingChange,
}: {
gardenId: number
canEdit: boolean
@@ -46,13 +48,23 @@ export function JournalPanel({
/** Which bed the panel is filtered to, if any. */
scopeObjectId: number | null
onScopeChange: (id: number | null) => void
+ /** Which single plop the panel is filtered to, if any (#85). Mutually
+ * exclusive with scopeObjectId — the editor store enforces that. */
+ scopePlantingId?: number | null
+ onScopePlantingChange?: (id: number | null) => void
}) {
// Date-range narrowing (#85): the backend and JournalFilter already supported
// from/to; they just had no UI. Empty inputs don't filter.
const [from, setFrom] = useState('')
const [to, setTo] = useState('')
+ // A plop scope wins over a bed scope — the two are mutually exclusive in the
+ // store, but guard here too so the filter is never double-scoped.
const filter = {
- ...(scopeObjectId != null ? { objectId: scopeObjectId } : {}),
+ ...(scopePlantingId != null
+ ? { plantingId: scopePlantingId }
+ : scopeObjectId != null
+ ? { objectId: scopeObjectId }
+ : {}),
...(from ? { from } : {}),
...(to ? { to } : {}),
}
@@ -83,6 +95,21 @@ export function JournalPanel({
)}
+ {scopePlantingId != null && (
+
+ Notes about one planting
+ {onScopePlantingChange && (
+ onScopePlantingChange(null)}
+ className="rounded px-1 text-muted outline-none hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/40"
+ >
+ Show all
+
+ )}
+
+ )}
+
From
@@ -121,8 +148,11 @@ export function JournalPanel({
{canEdit && (
)}
@@ -133,9 +163,11 @@ export function JournalPanel({
{journal.isSuccess && entries.length === 0 && (
- {scopedObject
- ? `Nothing written about ${objectDisplayName(scopedObject)} yet.`
- : 'Nothing written yet. This is where what happened goes — when something went in, what came up, what the frost got. Next year you get to read it back.'}
+ {scopePlantingId != null
+ ? 'Nothing written about this planting yet.'
+ : scopedObject
+ ? `Nothing written about ${objectDisplayName(scopedObject)} yet.`
+ : 'Nothing written yet. This is where what happened goes — when something went in, what came up, what the frost got. Next year you get to read it back.'}
)}
@@ -174,10 +206,13 @@ export function JournalPanel({
function Composer({
gardenId,
objectId,
+ plantingId = null,
scopeLabel,
}: {
gardenId: number
objectId: number | null
+ /** When set, the note attaches to this plop rather than a bed (#85). */
+ plantingId?: number | null
scopeLabel: string | null
}) {
const create = useCreateJournalEntry(gardenId)
@@ -191,7 +226,7 @@ function Composer({
if (!text) return
setError(null)
create.mutate(
- { body: text, observedAt, objectId: objectId ?? undefined },
+ { body: text, observedAt, objectId: objectId ?? undefined, plantingId: plantingId ?? undefined },
{
onSuccess: () => {
setBody('')
diff --git a/web/src/editor/PlopInspector.tsx b/web/src/editor/PlopInspector.tsx
index 6372034..5442427 100644
--- a/web/src/editor/PlopInspector.tsx
+++ b/web/src/editor/PlopInspector.tsx
@@ -25,6 +25,7 @@ export function PlopInspector({
unit,
onChangePlant,
onClose,
+ onAddNote,
readOnly = false,
}: {
plop: EditorPlanting
@@ -33,6 +34,9 @@ export function PlopInspector({
unit: UnitPref
onChangePlant: () => void
onClose: () => void
+ /** Scope the journal to this plop and open it — the plop parallel of the bed
+ * inspector's "add note" (#85). Absent for a viewer, who can't write notes. */
+ onAddNote?: () => void
readOnly?: boolean
}) {
const update = useUpdatePlanting(gardenId)
@@ -172,6 +176,12 @@ export function PlopInspector({
/>
+ {!readOnly && onAddNote && (
+
+ 📓 Add a note about this plant
+
+ )}
+
{!readOnly && (
void
+ // Which single plop the journal is filtered to, if any — the parallel of
+ // journalObjectId for a planting (#85). The two scopes are mutually exclusive
+ // (the setters clear each other), so the journal filter is never ambiguous.
+ journalPlantingId: number | null
+ setJournalPlantingId: (id: number | null) => void
+
// The plant armed for placing plops (set after the PlantPicker choice); stays
// armed for repeat-placement until cleared (Escape / done). null = not placing.
armedPlant: Plant | null
@@ -116,7 +122,11 @@ export const useEditorStore = create((set) => ({
setSeasonYear: (year) => set({ seasonYear: year }),
journalObjectId: null,
- setJournalObjectId: (id) => set({ journalObjectId: id }),
+ // Scoping to a bed clears any plop scope, so only one is ever active.
+ setJournalObjectId: (id) => set({ journalObjectId: id, journalPlantingId: null }),
+
+ journalPlantingId: null,
+ setJournalPlantingId: (id) => set({ journalPlantingId: id, journalObjectId: null }),
armedPlant: null,
armedLotId: null,
@@ -147,6 +157,7 @@ export const useEditorStore = create((set) => ({
railTab: null,
seasonYear: null,
journalObjectId: null,
+ journalPlantingId: null,
mode: DEFAULT_MODE,
}),
}))
diff --git a/web/src/pages/GardenEditorPage.tsx b/web/src/pages/GardenEditorPage.tsx
index b03b754..85ba221 100644
--- a/web/src/pages/GardenEditorPage.tsx
+++ b/web/src/pages/GardenEditorPage.tsx
@@ -85,6 +85,8 @@ export function GardenEditorPage() {
const setRailTab = useEditorStore((s) => s.setRailTab)
const journalObjectId = useEditorStore((s) => s.journalObjectId)
const setJournalObjectId = useEditorStore((s) => s.setJournalObjectId)
+ const journalPlantingId = useEditorStore((s) => s.journalPlantingId)
+ const setJournalPlantingId = useEditorStore((s) => s.setJournalPlantingId)
const journalCounts = useJournalCounts(gid)
const capabilities = useCapabilities()
const journalTotal = useMemo(
@@ -501,6 +503,13 @@ export function GardenEditorPage() {
readOnly={!canEdit}
onChangePlant={() => setPicker('change')}
onClose={() => selectPlanting(null)}
+ onAddNote={() => {
+ // Parity with the bed inspector: scope the journal to this plop and
+ // open it. The plop stays selected, so the selection effect keeps the
+ // inspector reachable when you switch back.
+ setJournalPlantingId(selectedPlop.id)
+ setRailTab('journal')
+ }}
/>
) : (
Select a bed or a planting to edit it.
@@ -521,6 +530,8 @@ export function GardenEditorPage() {
objects={objects}
scopeObjectId={journalObjectId}
onScopeChange={setJournalObjectId}
+ scopePlantingId={journalPlantingId}
+ onScopePlantingChange={setJournalPlantingId}
/>
),
},