Files
growstuff/app/javascript/components/AddJobModal.jsx
T
Brenda WallaceandDaniel O'Connor 75595350bc Easier recording of harvests from the front page (#5077)
* Implement AddSeedsPicker component and enhance planting selection UI; update styles and serializers for better seed management

* Enhance PlantingPicker component: add hints and titles for harvest and seed actions; update test to reflect changes in seed saving behavior

* Enhance harvest recording: allow harvesting from crops without plantings; update forms and modals for better user experience

* Refactor code structure for improved readability and maintainability

* Remove AddSeedsPicker component and its associated tests

* Add planting picker functionality for harvesting and seed saving

- Introduced PlantingPickersController to handle JSON responses for member's plantings.
- Updated HomeHelper to include crop ID in planting picker properties.
- Refactored PlantPicker and PlantSomethingModal components to support multiple gardens.
- Created PlantingFlow component to manage planting actions (harvest and save seeds).
- Implemented RecordDialog component to handle server-rendered links for harvesting and seed saving.
- Updated various views to include data attributes for triggering the record dialog.
- Added routes for planting picker and corresponding request specs for testing.
- Adjusted styles in _plantings.scss for better layout and spacing.

* Add job management features including job creation and wishlist functionality

* Remove WishlistJobsController and associated views and tests

* Add suggestion links rendering and fix routes formatting

* Refactor AddJobModal and JobPicker components to simplify props and enhance user experience

* Update seed addition test to check dialog presence and current path

* Apply suggestion from @CloCkWeRX

---------

Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
2026-10-26 21:38:12 +10:30

127 lines
4.7 KiB
React

import React, {useState} from 'react';
import {postJson} from '../api';
import {daysBefore, localToday} from '../dates';
import Modal from './Modal';
import WhenPicker from './WhenPicker';
const WHEN_CHOICES = [['today', 'Today'], ['tomorrow', 'Tomorrow'], ['other', 'Another day']];
function humanize(field) {
return field.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase());
}
function errorMessages(status, data) {
if (status === 422 && data && data.errors) {
const messages = Object.entries(data.errors).flatMap(([field, list]) => list.map((m) => `${humanize(field)} ${m}`));
if (messages.length > 0) return messages;
}
if (status === 401) return ['Please sign in again to add this job.'];
return ['Something went wrong saving that. Please try again.'];
}
// Adding a job, as a dialog over the page: what needs doing, when, and a note
// if they want one. Saves with POST /jobs.json and hands the job to onSaved.
export default function AddJobModal({onClose, onSaved}) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [noting, setNoting] = useState(false); // the notes box is open
const [when, setWhen] = useState('today');
const [customDate, setCustomDate] = useState('');
const [saving, setSaving] = useState(false);
const [errors, setErrors] = useState([]);
const today = localToday();
const dueOn = {today, tomorrow: daysBefore(today, -1), other: customDate}[when];
function chooseWhen(choice) {
setWhen(choice);
if (choice === 'other' && !customDate) setCustomDate(today);
}
async function save(event) {
event.preventDefault();
setSaving(true);
setErrors([]);
try {
const {ok, status, data} = await postJson('/jobs.json', {
job: {name, description, due_date: dueOn},
});
if (ok) {
onSaved(data);
return;
}
setErrors(errorMessages(status, data));
} catch (error) {
setErrors(['Couldn\'t reach the server. Please try again.']);
}
setSaving(false);
}
return (
<Modal title="Add a job" titleId="add-job-title" onClose={onClose}>
<form onSubmit={save}>
<div className="modal-body plant-dialog-body">
{errors.length > 0 && (
<div className="alert alert-danger" role="alert">
<i className="fas fa-exclamation-triangle" aria-hidden="true" />{' '}
<strong>That didn&rsquo;t save.</strong>
<ul className="mb-0">{errors.map((message) => <li key={message}>{message}</li>)}</ul>
</div>
)}
<div className="mb-3">
<label htmlFor="add-job-name" className="visually-hidden">What needs doing?</label>
<input
id="add-job-name"
className="form-control form-control-lg"
placeholder="What needs doing?"
aria-describedby="add-job-hint"
value={name}
onChange={(event) => setName(event.target.value)}
required
autoFocus
/>
<p id="add-job-hint" className="step-hint mb-0 mt-2">Water the seedlings, build a trellis for the peas…</p>
</div>
<WhenPicker
id="add-job"
legend="When?"
hideLegend
choices={WHEN_CHOICES}
when={when}
onWhen={chooseWhen}
date={customDate}
onDate={(event) => setCustomDate(event.target.value)}
hint={dueOn}
/>
{noting ? (
<div className="mt-3">
<label htmlFor="add-job-notes" className="visually-hidden">Notes</label>
<textarea
id="add-job-notes"
className="form-control"
rows="3"
placeholder="Note"
value={description}
onChange={(event) => setDescription(event.target.value)}
autoFocus
/>
</div>
) : (
<button type="button" className="btn btn-link ps-0 mt-2" onClick={() => setNoting(true)}>
<i className="fas fa-plus" aria-hidden="true" /> Add a note
</button>
)}
</div>
<div className="modal-footer">
<span className="me-auto step-hint"><i className="fas fa-globe" aria-hidden="true" /> Jobs are public</span>
<button type="button" className="btn btn-link" onClick={onClose} disabled={saving}>Cancel</button>
<button type="submit" className="btn btn-success btn-plant" disabled={saving} aria-busy={saving}>
{saving ? <><i className="fas fa-spinner fa-spin" aria-hidden="true" /> Saving…</> : 'Add job'}
</button>
</div>
</form>
</Modal>
);
}