Skip to content

Add validation to record entry modal#39

Open
34codezak wants to merge 1 commit intomainfrom
codex/add-input-handling-for-entry-forms
Open

Add validation to record entry modal#39
34codezak wants to merge 1 commit intomainfrom
codex/add-input-handling-for-entry-forms

Conversation

@34codezak
Copy link
Copy Markdown
Owner

@34codezak 34codezak commented Feb 9, 2026

Motivation

  • Prevent saving incomplete record entries by adding client-side validation and clear UX for required fields.
  • Improve form handling by making inputs controlled and resetting state when the modal closes.

Description

  • Add controlled form state formValues and touched along with isComplete to determine whether the form can be saved.
  • Reset form state on modal close with a React.useEffect and add handleChange, handleBlur, and handleSubmit handlers to manage interactions.
  • Convert the modal content to a <form onSubmit={handleSubmit}>, wire inputs to state, add id attributes and aria-invalid, and render inline required hints when fields are touched or on submit.
  • Disable the save button until isComplete is true and close the modal on successful submit.

Testing

  • Started the Next dev server and compiled the /manufacturing page successfully (Next fell back to a local font after Google Fonts fetch warnings).
  • Ran a Playwright script that opened http://127.0.0.1:3000/manufacturing, opened the record modal, and captured a screenshot; the script completed and produced the artifact successfully.

Codex Task

Summary by Sourcery

Add client-side validation and controlled form handling to the record entry modal to prevent saving incomplete entries and reset state on close.

New Features:

  • Introduce controlled form state and validation for record entry inputs in the modal.
  • Disable saving a record entry until all required fields are completed and valid.

Enhancements:

  • Reset form field values and touched state whenever the modal is closed to avoid stale input.
  • Improve accessibility of the record entry form with associated labels, ids, and aria-invalid attributes on inputs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
erp-system Error Error Feb 9, 2026 11:15am

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Feb 9, 2026

Reviewer's Guide

Adds controlled form state and client-side validation to the record entry modal, wiring inputs into a form submission flow that disables save until all fields are complete and resets state when the modal closes.

Sequence diagram for record entry modal validation and submission

sequenceDiagram
  actor User
  participant RecordEntryCard
  participant ReactState
  participant ModalUI

  User->>RecordEntryCard: Click open record entry modal
  RecordEntryCard->>ReactState: setOpen(true)
  ReactState-->>RecordEntryCard: open=true
  RecordEntryCard->>ModalUI: Render modal with controlled inputs

  User->>ModalUI: Type into input field
  ModalUI->>RecordEntryCard: onChange(field, value)
  RecordEntryCard->>ReactState: setFormValues(prev, field, value)
  ReactState-->>RecordEntryCard: formValues updated
  RecordEntryCard->>ModalUI: Re-render with new input value

  User->>ModalUI: Blur input field
  ModalUI->>RecordEntryCard: onBlur(field)
  RecordEntryCard->>ReactState: setTouched(prev, field, true)
  ReactState-->>RecordEntryCard: touched updated
  RecordEntryCard->>RecordEntryCard: Compute isComplete from formValues
  RecordEntryCard->>ModalUI: Update aria-invalid and error hint
  RecordEntryCard->>ModalUI: Enable or disable Save button based on isComplete

  User->>ModalUI: Click Save entry
  ModalUI->>RecordEntryCard: handleSubmit(event)
  RecordEntryCard->>RecordEntryCard: Prevent default form submission
  RecordEntryCard->>ReactState: setTouched(all fields true)
  RecordEntryCard->>RecordEntryCard: Check isComplete
  alt Form incomplete
    RecordEntryCard->>ModalUI: Show required field messages
  else Form complete
    RecordEntryCard->>ReactState: setOpen(false)
  end

  ReactState-->>RecordEntryCard: open=false
  RecordEntryCard->>RecordEntryCard: useEffect(open) resets formValues and touched
  RecordEntryCard->>ModalUI: Modal unmounts with cleared form state
Loading

Class diagram for RecordEntryCard component state and handlers

classDiagram
  class RecordEntryCard {
    +boolean open
    +FormValues formValues
    +Touched touched
    +boolean isComplete
    +handleChange(field, value)
    +handleBlur(field)
    +handleSubmit(event)
  }

  class FormValues {
    +string entryName
    +string value
    +string notes
  }

  class Touched {
    +boolean entryName
    +boolean value
    +boolean notes
  }

  RecordEntryCard --> FormValues : uses
  RecordEntryCard --> Touched : uses
Loading

File-Level Changes

Change Details Files
Introduce controlled form state and completion logic for the record entry modal inputs.
  • Add formValues state for entryName, value, and notes fields initialized to empty strings.
  • Add touched state to track blur/interaction for each field.
  • Derive an isComplete flag by checking that all formValues fields are non-empty after trimming whitespace.
components/record-entry-card.tsx
Reset form state whenever the modal is closed.
  • Add a React.useEffect that watches the open state.
  • When open becomes false, reset formValues and touched to their initial empty/untouched values.
components/record-entry-card.tsx
Wire modal content into a real form with validation-aware inputs and submit handling.
  • Convert the modal body container from a div to a form element with an onSubmit handler.
  • Implement handleChange, handleBlur, and handleSubmit handlers to manage input changes, blur/touched state, and form submission.
  • On submit, mark all fields touched, prevent default submission, and close the modal only when isComplete is true.
components/record-entry-card.tsx
Enhance accessibility and inline validation feedback for form fields.
  • Add id attributes and associated htmlFor labels for entryName, value, and notes inputs.
  • Set aria-invalid on inputs and textarea based on touched status and whether the value is non-empty.
  • Render inline text hints for each field indicating it is required when touched but empty.
components/record-entry-card.tsx
Gate the save action on form completeness instead of allowing unconditional saves.
  • Change the Save entry button from type="button" to type="submit".
  • Disable the Save entry button when isComplete is false so users cannot submit incomplete forms.
components/record-entry-card.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • You repeat the initial state objects for formValues and touched in both useState and the useEffect reset; consider extracting these into shared constants or a small initializer function to avoid duplication and keep them in sync.
  • Right now aria-invalid is always set to a boolean which becomes the string "false" when valid; it would be more accessible to only set aria-invalid when a field is actually invalid (e.g., aria-invalid={isInvalid || undefined}) and optionally wire the inline error text via aria-describedby.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- You repeat the initial state objects for `formValues` and `touched` in both `useState` and the `useEffect` reset; consider extracting these into shared constants or a small initializer function to avoid duplication and keep them in sync.
- Right now `aria-invalid` is always set to a boolean which becomes the string "false" when valid; it would be more accessible to only set `aria-invalid` when a field is actually invalid (e.g., `aria-invalid={isInvalid || undefined}`) and optionally wire the inline error text via `aria-describedby`.

## Individual Comments

### Comment 1
<location> `components/record-entry-card.tsx:23` </location>
<code_context>
   children
 }: RecordEntryCardProps) {
   const [open, setOpen] = React.useState(false);
+  const [formValues, setFormValues] = React.useState({
+    entryName: "",
+    value: "",
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting shared form state, validation, and field definitions into reusable helpers and a config map to avoid duplicating similar logic for each field.

You can keep all the current behavior but significantly reduce repetition by:

1. Centralizing initial state and reset logic.
2. Centralizing validation.
3. Driving fields from a config map instead of inlined per-field wiring.

### 1. Centralize initial state and reset

Instead of repeating shapes in `useState` and `useEffect`:

```ts
const [formValues, setFormValues] = React.useState({
  entryName: "",
  value: "",
  notes: ""
});
const [touched, setTouched] = React.useState({
  entryName: false,
  value: false,
  notes: false
});

React.useEffect(() => {
  if (!open) {
    setFormValues({ entryName: "", value: "", notes: "" });
    setTouched({ entryName: false, value: false, notes: false });
  }
}, [open]);
```

extract the initial objects and reuse them:

```ts
const initialFormValues = { entryName: "", value: "", notes: "" } as const;
const initialTouched = { entryName: false, value: false, notes: false } as const;

const [formValues, setFormValues] = React.useState(initialFormValues);
const [touched, setTouched] = React.useState(initialTouched);

React.useEffect(() => {
  if (!open) {
    setFormValues(initialFormValues);
    setTouched(initialTouched);
  }
}, [open]);
```

This also makes it trivial to extend with more fields later.

### 2. Centralize validation

The repeated `touched.<field> && !formValues.<field>.trim()` can be wrapped in a helper:

```ts
type FieldName = keyof typeof formValues;

const isFieldInvalid = (field: FieldName) =>
  touched[field] && !formValues[field].trim();
```

Then your inputs become:

```tsx
<input
  // ...
  aria-invalid={isFieldInvalid("entryName")}
/>
{isFieldInvalid("entryName") && (
  <p className="text-xs text-amber-600">Entry name is required.</p>
)}
```

Same for `value` and `notes`. This pulls the validation rule into a single place.

### 3. Drive fields from a small config

You can avoid repeating the `value/onChange/onBlur/aria-invalid` wiring by using a field config and `.map`:

```ts
const fields = [
  {
    name: "entryName" as const,
    label: "Entry name",
    id: "entry-name",
    placeholder: `Add ${entryTitle} reference`,
    as: "input" as const
  },
  {
    name: "value" as const,
    label: "Value",
    id: "entry-value",
    placeholder: "Enter a value",
    as: "input" as const
  },
  {
    name: "notes" as const,
    label: "Notes",
    id: "entry-notes",
    placeholder: "Add context for the record",
    as: "textarea" as const
  }
];
```

Render them uniformly:

```tsx
<div className="space-y-3 text-sm">
  {fields.map(field => (
    <div key={field.name} className="space-y-1">
      <label
        htmlFor={field.id}
        className="text-xs font-medium text-slate-500"
      >
        {field.label}
      </label>

      {field.as === "input" ? (
        <input
          id={field.id}
          type="text"
          placeholder={field.placeholder}
          value={formValues[field.name]}
          onChange={e => handleChange(field.name, e.target.value)}
          onBlur={() => handleBlur(field.name)}
          aria-invalid={isFieldInvalid(field.name)}
          className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-slate-300 focus:outline-none"
        />
      ) : (
        <textarea
          id={field.id}
          rows={3}
          placeholder={field.placeholder}
          value={formValues[field.name]}
          onChange={e => handleChange(field.name, e.target.value)}
          onBlur={() => handleBlur(field.name)}
          aria-invalid={isFieldInvalid(field.name)}
          className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-slate-300 focus:outline-none"
        />
      )}

      {isFieldInvalid(field.name) && (
        <p className="text-xs text-amber-600">
          {field.label} {field.name === "notes" ? "are" : "is"} required.
        </p>
      )}
    </div>
  ))}
</div>
```

This keeps all your current behavior (per-field touch, per-field errors, disabled submit, reset on close) but:

- Removes repeated handler/validation wiring.
- Centralizes field metadata.
- Makes adding new fields a one-line config change instead of duplicating JSX + logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

children
}: RecordEntryCardProps) {
const [open, setOpen] = React.useState(false);
const [formValues, setFormValues] = React.useState({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting shared form state, validation, and field definitions into reusable helpers and a config map to avoid duplicating similar logic for each field.

You can keep all the current behavior but significantly reduce repetition by:

  1. Centralizing initial state and reset logic.
  2. Centralizing validation.
  3. Driving fields from a config map instead of inlined per-field wiring.

1. Centralize initial state and reset

Instead of repeating shapes in useState and useEffect:

const [formValues, setFormValues] = React.useState({
  entryName: "",
  value: "",
  notes: ""
});
const [touched, setTouched] = React.useState({
  entryName: false,
  value: false,
  notes: false
});

React.useEffect(() => {
  if (!open) {
    setFormValues({ entryName: "", value: "", notes: "" });
    setTouched({ entryName: false, value: false, notes: false });
  }
}, [open]);

extract the initial objects and reuse them:

const initialFormValues = { entryName: "", value: "", notes: "" } as const;
const initialTouched = { entryName: false, value: false, notes: false } as const;

const [formValues, setFormValues] = React.useState(initialFormValues);
const [touched, setTouched] = React.useState(initialTouched);

React.useEffect(() => {
  if (!open) {
    setFormValues(initialFormValues);
    setTouched(initialTouched);
  }
}, [open]);

This also makes it trivial to extend with more fields later.

2. Centralize validation

The repeated touched.<field> && !formValues.<field>.trim() can be wrapped in a helper:

type FieldName = keyof typeof formValues;

const isFieldInvalid = (field: FieldName) =>
  touched[field] && !formValues[field].trim();

Then your inputs become:

<input
  // ...
  aria-invalid={isFieldInvalid("entryName")}
/>
{isFieldInvalid("entryName") && (
  <p className="text-xs text-amber-600">Entry name is required.</p>
)}

Same for value and notes. This pulls the validation rule into a single place.

3. Drive fields from a small config

You can avoid repeating the value/onChange/onBlur/aria-invalid wiring by using a field config and .map:

const fields = [
  {
    name: "entryName" as const,
    label: "Entry name",
    id: "entry-name",
    placeholder: `Add ${entryTitle} reference`,
    as: "input" as const
  },
  {
    name: "value" as const,
    label: "Value",
    id: "entry-value",
    placeholder: "Enter a value",
    as: "input" as const
  },
  {
    name: "notes" as const,
    label: "Notes",
    id: "entry-notes",
    placeholder: "Add context for the record",
    as: "textarea" as const
  }
];

Render them uniformly:

<div className="space-y-3 text-sm">
  {fields.map(field => (
    <div key={field.name} className="space-y-1">
      <label
        htmlFor={field.id}
        className="text-xs font-medium text-slate-500"
      >
        {field.label}
      </label>

      {field.as === "input" ? (
        <input
          id={field.id}
          type="text"
          placeholder={field.placeholder}
          value={formValues[field.name]}
          onChange={e => handleChange(field.name, e.target.value)}
          onBlur={() => handleBlur(field.name)}
          aria-invalid={isFieldInvalid(field.name)}
          className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-slate-300 focus:outline-none"
        />
      ) : (
        <textarea
          id={field.id}
          rows={3}
          placeholder={field.placeholder}
          value={formValues[field.name]}
          onChange={e => handleChange(field.name, e.target.value)}
          onBlur={() => handleBlur(field.name)}
          aria-invalid={isFieldInvalid(field.name)}
          className="w-full rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:border-slate-300 focus:outline-none"
        />
      )}

      {isFieldInvalid(field.name) && (
        <p className="text-xs text-amber-600">
          {field.label} {field.name === "notes" ? "are" : "is"} required.
        </p>
      )}
    </div>
  ))}
</div>

This keeps all your current behavior (per-field touch, per-field errors, disabled submit, reset on close) but:

  • Removes repeated handler/validation wiring.
  • Centralizes field metadata.
  • Makes adding new fields a one-line config change instead of duplicating JSX + logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant