Merge pull request 'Let plop notes be written from the UI (#85 item 5)' (#123) from feat/plop-journal-notes into main
Build image / build-and-push (push) Successful in 13s
Build image / build-and-push (push) Successful in 13s
This commit was merged in pull request #123.
This commit is contained in:
@@ -37,6 +37,8 @@ export function JournalPanel({
|
|||||||
objects,
|
objects,
|
||||||
scopeObjectId,
|
scopeObjectId,
|
||||||
onScopeChange,
|
onScopeChange,
|
||||||
|
scopePlantingId,
|
||||||
|
onScopePlantingChange,
|
||||||
}: {
|
}: {
|
||||||
gardenId: number
|
gardenId: number
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
@@ -46,19 +48,30 @@ export function JournalPanel({
|
|||||||
/** Which bed the panel is filtered to, if any. */
|
/** Which bed the panel is filtered to, if any. */
|
||||||
scopeObjectId: number | null
|
scopeObjectId: number | null
|
||||||
onScopeChange: (id: number | null) => void
|
onScopeChange: (id: number | null) => void
|
||||||
|
/** Which single plop the panel is filtered to, if any (#85). The store keeps
|
||||||
|
* this mutually exclusive with scopeObjectId. Required like its bed twin. */
|
||||||
|
scopePlantingId: number | null
|
||||||
|
onScopePlantingChange: (id: number | null) => void
|
||||||
}) {
|
}) {
|
||||||
// Date-range narrowing (#85): the backend and JournalFilter already supported
|
// Date-range narrowing (#85): the backend and JournalFilter already supported
|
||||||
// from/to; they just had no UI. Empty inputs don't filter.
|
// from/to; they just had no UI. Empty inputs don't filter.
|
||||||
const [from, setFrom] = useState('')
|
const [from, setFrom] = useState('')
|
||||||
const [to, setTo] = useState('')
|
const [to, setTo] = useState('')
|
||||||
|
const scopedObject = objects.find((o) => o.id === scopeObjectId) ?? null
|
||||||
|
// One source of scope priority — plop over bed — for both the filter and the
|
||||||
|
// composer's label, so they can't drift.
|
||||||
|
const scopeLabel = scopePlantingId != null ? 'this planting' : scopedObject ? objectDisplayName(scopedObject) : null
|
||||||
const filter = {
|
const filter = {
|
||||||
...(scopeObjectId != null ? { objectId: scopeObjectId } : {}),
|
...(scopePlantingId != null
|
||||||
|
? { plantingId: scopePlantingId }
|
||||||
|
: scopeObjectId != null
|
||||||
|
? { objectId: scopeObjectId }
|
||||||
|
: {}),
|
||||||
...(from ? { from } : {}),
|
...(from ? { from } : {}),
|
||||||
...(to ? { to } : {}),
|
...(to ? { to } : {}),
|
||||||
}
|
}
|
||||||
const journal = useJournal(gardenId, filter)
|
const journal = useJournal(gardenId, filter)
|
||||||
const entries = journal.data?.pages.flatMap((p) => p.entries) ?? []
|
const entries = journal.data?.pages.flatMap((p) => p.entries) ?? []
|
||||||
const scopedObject = objects.find((o) => o.id === scopeObjectId) ?? null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@@ -83,6 +96,19 @@ export function JournalPanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{scopePlantingId != null && (
|
||||||
|
<div className="flex items-center justify-between gap-2 rounded-md bg-accent/10 px-2 py-1 text-xs">
|
||||||
|
<span className="text-accent-strong">Notes about one planting</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onScopePlantingChange(null)}
|
||||||
|
className="rounded px-1 text-muted outline-none hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||||
|
>
|
||||||
|
Show all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted">
|
<div className="flex items-center gap-2 text-xs text-muted">
|
||||||
<label className="flex items-center gap-1">
|
<label className="flex items-center gap-1">
|
||||||
<span>From</span>
|
<span>From</span>
|
||||||
@@ -121,8 +147,9 @@ export function JournalPanel({
|
|||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Composer
|
<Composer
|
||||||
gardenId={gardenId}
|
gardenId={gardenId}
|
||||||
objectId={scopeObjectId}
|
objectId={scopePlantingId != null ? null : scopeObjectId}
|
||||||
scopeLabel={scopedObject ? objectDisplayName(scopedObject) : null}
|
plantingId={scopePlantingId}
|
||||||
|
scopeLabel={scopeLabel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -133,9 +160,11 @@ export function JournalPanel({
|
|||||||
|
|
||||||
{journal.isSuccess && entries.length === 0 && (
|
{journal.isSuccess && entries.length === 0 && (
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
{scopedObject
|
{scopePlantingId != null
|
||||||
? `Nothing written about ${objectDisplayName(scopedObject)} yet.`
|
? 'Nothing written about this planting 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.'}
|
: 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.'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -174,10 +203,13 @@ export function JournalPanel({
|
|||||||
function Composer({
|
function Composer({
|
||||||
gardenId,
|
gardenId,
|
||||||
objectId,
|
objectId,
|
||||||
|
plantingId = null,
|
||||||
scopeLabel,
|
scopeLabel,
|
||||||
}: {
|
}: {
|
||||||
gardenId: number
|
gardenId: number
|
||||||
objectId: number | null
|
objectId: number | null
|
||||||
|
/** When set, the note attaches to this plop rather than a bed (#85). */
|
||||||
|
plantingId?: number | null
|
||||||
scopeLabel: string | null
|
scopeLabel: string | null
|
||||||
}) {
|
}) {
|
||||||
const create = useCreateJournalEntry(gardenId)
|
const create = useCreateJournalEntry(gardenId)
|
||||||
@@ -191,7 +223,7 @@ function Composer({
|
|||||||
if (!text) return
|
if (!text) return
|
||||||
setError(null)
|
setError(null)
|
||||||
create.mutate(
|
create.mutate(
|
||||||
{ body: text, observedAt, objectId: objectId ?? undefined },
|
{ body: text, observedAt, objectId: objectId ?? undefined, plantingId: plantingId ?? undefined },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setBody('')
|
setBody('')
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function PlopInspector({
|
|||||||
unit,
|
unit,
|
||||||
onChangePlant,
|
onChangePlant,
|
||||||
onClose,
|
onClose,
|
||||||
|
onAddNote,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
}: {
|
}: {
|
||||||
plop: EditorPlanting
|
plop: EditorPlanting
|
||||||
@@ -33,6 +34,11 @@ export function PlopInspector({
|
|||||||
unit: UnitPref
|
unit: UnitPref
|
||||||
onChangePlant: () => void
|
onChangePlant: () => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
/** Scope the journal to this plop and open it — the plop parallel of the bed
|
||||||
|
* inspector's "add note" (#85). Offered to viewers too (to READ the plop's
|
||||||
|
* notes, like the bed inspector does); the journal's composer is separately
|
||||||
|
* gated on edit rights, so a viewer just sees the entries. */
|
||||||
|
onAddNote?: () => void
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
}) {
|
}) {
|
||||||
const update = useUpdatePlanting(gardenId)
|
const update = useUpdatePlanting(gardenId)
|
||||||
@@ -172,6 +178,12 @@ export function PlopInspector({
|
|||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{onAddNote && (
|
||||||
|
<Button variant="ghost" className="justify-start px-2 py-1 text-sm" onClick={onAddNote}>
|
||||||
|
📓 {readOnly ? 'Notes about this plant' : 'Add a note about this plant'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
+12
-1
@@ -59,6 +59,12 @@ interface EditorState {
|
|||||||
journalObjectId: number | null
|
journalObjectId: number | null
|
||||||
setJournalObjectId: (id: number | null) => void
|
setJournalObjectId: (id: number | null) => 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
|
// The plant armed for placing plops (set after the PlantPicker choice); stays
|
||||||
// armed for repeat-placement until cleared (Escape / done). null = not placing.
|
// armed for repeat-placement until cleared (Escape / done). null = not placing.
|
||||||
armedPlant: Plant | null
|
armedPlant: Plant | null
|
||||||
@@ -116,7 +122,11 @@ export const useEditorStore = create<EditorState>((set) => ({
|
|||||||
setSeasonYear: (year) => set({ seasonYear: year }),
|
setSeasonYear: (year) => set({ seasonYear: year }),
|
||||||
|
|
||||||
journalObjectId: null,
|
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,
|
armedPlant: null,
|
||||||
armedLotId: null,
|
armedLotId: null,
|
||||||
@@ -147,6 +157,7 @@ export const useEditorStore = create<EditorState>((set) => ({
|
|||||||
railTab: null,
|
railTab: null,
|
||||||
seasonYear: null,
|
seasonYear: null,
|
||||||
journalObjectId: null,
|
journalObjectId: null,
|
||||||
|
journalPlantingId: null,
|
||||||
mode: DEFAULT_MODE,
|
mode: DEFAULT_MODE,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export function GardenEditorPage() {
|
|||||||
const setRailTab = useEditorStore((s) => s.setRailTab)
|
const setRailTab = useEditorStore((s) => s.setRailTab)
|
||||||
const journalObjectId = useEditorStore((s) => s.journalObjectId)
|
const journalObjectId = useEditorStore((s) => s.journalObjectId)
|
||||||
const setJournalObjectId = useEditorStore((s) => s.setJournalObjectId)
|
const setJournalObjectId = useEditorStore((s) => s.setJournalObjectId)
|
||||||
|
const journalPlantingId = useEditorStore((s) => s.journalPlantingId)
|
||||||
|
const setJournalPlantingId = useEditorStore((s) => s.setJournalPlantingId)
|
||||||
const journalCounts = useJournalCounts(gid)
|
const journalCounts = useJournalCounts(gid)
|
||||||
const capabilities = useCapabilities()
|
const capabilities = useCapabilities()
|
||||||
const journalTotal = useMemo(
|
const journalTotal = useMemo(
|
||||||
@@ -502,6 +504,13 @@ export function GardenEditorPage() {
|
|||||||
readOnly={!canEdit}
|
readOnly={!canEdit}
|
||||||
onChangePlant={() => setPicker('change')}
|
onChangePlant={() => setPicker('change')}
|
||||||
onClose={() => selectPlanting(null)}
|
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')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted">Select a bed or a planting to edit it.</p>
|
<p className="text-sm text-muted">Select a bed or a planting to edit it.</p>
|
||||||
@@ -522,6 +531,8 @@ export function GardenEditorPage() {
|
|||||||
objects={objects}
|
objects={objects}
|
||||||
scopeObjectId={journalObjectId}
|
scopeObjectId={journalObjectId}
|
||||||
onScopeChange={setJournalObjectId}
|
onScopeChange={setJournalObjectId}
|
||||||
|
scopePlantingId={journalPlantingId}
|
||||||
|
onScopePlantingChange={setJournalPlantingId}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user