[#398] COMPONENTS - Added creation/edit dialog component#421
[#398] COMPONENTS - Added creation/edit dialog component#421
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new client-side CreateEditDialog React component implementing a portal modal for creating/editing Issues in Event or Task modes, plus related types/constants (CreateEditDialogProps, IssueFormValues, EventFormValues, DetailSectionKey, SECTION_TABS, and status/priority types) consumed by the dialog. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Dialog as CreateEditDialog
participant Parent as ParentComponent
participant API as onSubmit/onDelete
User->>Dialog: open(intent=create|edit, initialValues?)
Dialog-->>User: render modal (tabs, Event/Task fields, controls)
User->>Dialog: change fields / toggle isEvent / add/remove links
Dialog->>Dialog: update internal form state
User->>Dialog: Submit
Dialog->>Parent: onSubmit(formValues)
Parent->>API: persist create/update
API-->>Parent: success/failure
Parent-->>Dialog: onClose() or surface errors
User->>Dialog: Delete (when editing)
Dialog->>Parent: onDelete(formValues)
Parent->>API: delete
API-->>Parent: success
Parent-->>Dialog: onClose()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Suggested labels
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.Change the |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)
706-718: Add accessible labels to dynamic link inputs.The link label and URL inputs lack associated
<Label>elements oraria-labelattributes. Screen reader users won't know what these fields are for.Add aria-labels to link inputs
<Input className={cn(baseField, "h-11")} placeholder="Label (e.g., Notion doc)" + aria-label={`Link ${formValues.links.indexOf(link) + 1} label`} value={link.label} onChange={(event) => handleLinkUpdate( link.id, "label", event.target.value, ) } /><Input className={cn(baseField, "h-11 pl-9")} placeholder="URL" type="url" + aria-label={`Link ${formValues.links.indexOf(link) + 1} URL`} value={link.url} onChange={(event) => handleLinkUpdate( link.id, "url", event.target.value, ) } />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 706 - 718, The dynamic link inputs rendered inside the create-edit dialog are missing accessible labels; update the Input elements (the ones using value={link.label} and value={link.url} and onChange calling handleLinkUpdate(link.id,...)) to include accessible labeling — either wrap each input with the existing Label component or add descriptive aria-label attributes (e.g., aria-label="Link label" and aria-label="Link URL") and keep the existing className={cn(baseField, "h-11")}; ensure the label text or aria-label uniquely identifies the field when iterating multiple links so screen readers can distinguish them (you can include the link.id or index in the aria-label if needed).
300-331: Addaria-labelledbyto improve screen reader accessibility.The dialog has
role="dialog"andaria-modal, but screen readers benefit fromaria-labelledbypointing to the title. This helps users understand the dialog's purpose when it opens.Add aria-labelledby
+ const titleId = `${baseId}-dialog-title`; + // ... in JSX: <div aria-modal role="dialog" + aria-labelledby={titleId} className="fixed inset-0 z-50 ..." onMouseDown={handleOverlayPointerDown} > ... - <h2 className="text-2xl font-semibold text-white"> + <h2 id={titleId} className="text-2xl font-semibold text-white">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 300 - 331, The dialog container with role="dialog" lacks aria-labelledby; add aria-labelledby to the outer div returned by createPortal (the element with role="dialog") and give the title h2 a matching id (e.g., id="create-edit-dialog-title") so screen readers announce the dialog title; update the h2 inside the header (the element rendering "Update the event details below" / "Enter the event details below") and the createPortal div (the element with aria-modal and role="dialog") accordingly.
285-288: Consider validating date range before submission.The form allows
endDate/endTimeto be beforestartDate/startTime. Adding client-side validation prevents users from submitting logically invalid date ranges.Example validation in handleSubmit
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); + + if (!formValues.allDay) { + const start = new Date(`${formValues.startDate}T${formValues.startTime}`); + const end = new Date(`${formValues.endDate}T${formValues.endTime}`); + if (end <= start) { + // Handle error - could add error state and display message + return; + } + } + onSubmit?.(formValues); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 285 - 288, The handleSubmit function currently submits formValues without checking that the end date/time is after the start; update handleSubmit to validate the date range by constructing comparable Date objects from formValues.startDate/formValues.startTime and formValues.endDate/formValues.endTime, and if the end is not after the start prevent submission (call event.preventDefault() already present), set a validation error state or invoke existing error handler to show a user-facing message, and only call onSubmit(formValues) when the date-range check passes; reference the handleSubmit function, formValues fields (startDate, startTime, endDate, endTime), and onSubmit when implementing this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 737-744: The delete button that renders the Trash2 icon lacks an
accessible name; update the Button element that calls handleRemoveLink(link.id)
to include an aria-label (for example aria-label={`Delete link${link.title ? `:
${link.title}` : ''}`} or a static "Delete link") so screen readers get context
when they encounter the icon-only button.
- Around line 821-827: formatDateForInput currently uses date.toISOString()
(UTC) while formatTimeForInput uses date.toTimeString() (local), causing
timezone drift; change formatDateForInput to produce a local YYYY-MM-DD string
(e.g., via date.getFullYear()/getMonth()+1/getDate with zero-padding or
date.toLocaleDateString('sv')/ 'en-CA') so both formatDateForInput and
formatTimeForInput use the same local time basis and remain consistent across
timezones.
- Around line 31-49: IssueFormValues fields and STATUS_OPTIONS values do not
match the Issue DB enum (ISSUE.ISSUE_STATUS) and other form fields don't map to
the Issue model; before calling createIssue you must transform the form payload
to the DB shape: map form status values (from STATUS_OPTIONS) to the DB enum
values (e.g., "confirmed"/"tentative"/"draft"/"cancelled" -> one of
BACKLOG/PLANNING/IN_PROGRESS/FINISHED), combine startDate+startTime and
endDate+endTime into the single timestamp column (date) or otherwise produce the
expected timestamp(s), convert LinkItem[] into a string[] (text[])
representation, and drop or persist the extra properties (priority,
requiresRoom, needsDesignAssets, needsOutreach, requirements, notes) either by
removing them from the payload sent to createIssue or by adding corresponding DB
columns if intended; update the code path that builds the mutation payload
(where IssueFormValues is serialized for createIssue) to perform these mappings
and validations.
---
Nitpick comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 706-718: The dynamic link inputs rendered inside the create-edit
dialog are missing accessible labels; update the Input elements (the ones using
value={link.label} and value={link.url} and onChange calling
handleLinkUpdate(link.id,...)) to include accessible labeling — either wrap each
input with the existing Label component or add descriptive aria-label attributes
(e.g., aria-label="Link label" and aria-label="Link URL") and keep the existing
className={cn(baseField, "h-11")}; ensure the label text or aria-label uniquely
identifies the field when iterating multiple links so screen readers can
distinguish them (you can include the link.id or index in the aria-label if
needed).
- Around line 300-331: The dialog container with role="dialog" lacks
aria-labelledby; add aria-labelledby to the outer div returned by createPortal
(the element with role="dialog") and give the title h2 a matching id (e.g.,
id="create-edit-dialog-title") so screen readers announce the dialog title;
update the h2 inside the header (the element rendering "Update the event details
below" / "Enter the event details below") and the createPortal div (the element
with aria-modal and role="dialog") accordingly.
- Around line 285-288: The handleSubmit function currently submits formValues
without checking that the end date/time is after the start; update handleSubmit
to validate the date range by constructing comparable Date objects from
formValues.startDate/formValues.startTime and
formValues.endDate/formValues.endTime, and if the end is not after the start
prevent submission (call event.preventDefault() already present), set a
validation error state or invoke existing error handler to show a user-facing
message, and only call onSubmit(formValues) when the date-range check passes;
reference the handleSubmit function, formValues fields (startDate, startTime,
endDate, endTime), and onSubmit when implementing this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 5cc81bde-1b80-4cdc-9cd4-6d1246535d03
📒 Files selected for processing (1)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx
| <Button | ||
| variant="ghost" | ||
| type="button" | ||
| className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white" | ||
| onClick={() => handleRemoveLink(link.id)} | ||
| > | ||
| <Trash2 className="h-4 w-4" /> | ||
| </Button> |
There was a problem hiding this comment.
Add aria-label to the delete link button.
The button only contains an icon. Screen readers will announce it as "button" with no context.
Add aria-label
<Button
variant="ghost"
type="button"
className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
onClick={() => handleRemoveLink(link.id)}
+ aria-label={`Remove link ${link.label || "item"}`}
>
<Trash2 className="h-4 w-4" />
</Button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Button | |
| variant="ghost" | |
| type="button" | |
| className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white" | |
| onClick={() => handleRemoveLink(link.id)} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| type="button" | |
| className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white" | |
| onClick={() => handleRemoveLink(link.id)} | |
| aria-label={`Remove link ${link.label || "item"}`} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
737 - 744, The delete button that renders the Trash2 icon lacks an accessible
name; update the Button element that calls handleRemoveLink(link.id) to include
an aria-label (for example aria-label={`Delete link${link.title ? `:
${link.title}` : ''}`} or a static "Delete link") so screen readers get context
when they encounter the icon-only button.
| function formatDateForInput(date: Date) { | ||
| return date.toISOString().slice(0, 10); | ||
| } | ||
|
|
||
| function formatTimeForInput(date: Date) { | ||
| return date.toTimeString().slice(0, 5); | ||
| } |
There was a problem hiding this comment.
Timezone inconsistency between date and time formatting.
formatDateForInput uses toISOString() which returns UTC, but formatTimeForInput uses toTimeString() which returns local time. This mismatch can cause the displayed date to be off by one day near midnight in certain timezones.
Use consistent local time formatting
function formatDateForInput(date: Date) {
- return date.toISOString().slice(0, 10);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function formatDateForInput(date: Date) { | |
| return date.toISOString().slice(0, 10); | |
| } | |
| function formatTimeForInput(date: Date) { | |
| return date.toTimeString().slice(0, 5); | |
| } | |
| function formatDateForInput(date: Date) { | |
| const year = date.getFullYear(); | |
| const month = String(date.getMonth() + 1).padStart(2, "0"); | |
| const day = String(date.getDate()).padStart(2, "0"); | |
| return `${year}-${month}-${day}`; | |
| } | |
| function formatTimeForInput(date: Date) { | |
| return date.toTimeString().slice(0, 5); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
821 - 827, formatDateForInput currently uses date.toISOString() (UTC) while
formatTimeForInput uses date.toTimeString() (local), causing timezone drift;
change formatDateForInput to produce a local YYYY-MM-DD string (e.g., via
date.getFullYear()/getMonth()+1/getDate with zero-padding or
date.toLocaleDateString('sv')/ 'en-CA') so both formatDateForInput and
formatTimeForInput use the same local time basis and remain consistent across
timezones.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)
854-861:⚠️ Potential issue | 🟡 MinorAdd an accessible name to the icon-only delete button.
At Line 854, the trash-icon button has no
aria-label, so assistive tech lacks context.Suggested patch
<Button variant="ghost" type="button" className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white" onClick={() => handleRemoveLink(link.id)} + aria-label={`Remove link ${link.label || "item"}`} > <Trash2 className="h-4 w-4" /> </Button>As per coding guidelines,
apps/blade/**requires accessibility-safe UI patterns (including ARIA coverage for actionable controls).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 854 - 861, The trash-icon-only Button (Button with onClick={() => handleRemoveLink(link.id)} rendering <Trash2 />) is missing an accessible name; add an aria-label (or aria-labelledby) to the Button so screen readers have context (e.g., aria-label={`Delete link`} or a dynamic label like aria-label={`Delete link ${link.title || link.url || link.id}`}) while keeping the existing onClick handler and classes; update the Button that wraps <Trash2 className="h-4 w-4" /> to include this aria attribute so the control is accessible.
938-944:⚠️ Potential issue | 🟡 MinorUse a consistent local-time basis for date/time formatting.
Line 939 uses UTC (
toISOString) while Line 943 uses local time (toTimeString), which can shift the displayed date near midnight/timezone boundaries.Suggested patch
function formatDateForInput(date: Date) { - return date.toISOString().slice(0, 10); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; }In JavaScript, does Date.prototype.toISOString() always output UTC while Date.prototype.toTimeString() outputs local time?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 938 - 944, formatDateForInput uses UTC via toISOString() while formatTimeForInput uses local time via toTimeString(), causing inconsistent results near timezone boundaries; change both to use the same local-time basis by deriving date and time from the Date object's local getters (e.g., getFullYear, getMonth, getDate for formatDateForInput and getHours, getMinutes for formatTimeForInput) and zero-pad as needed so both functions consistently format based on local time; update the implementations of formatDateForInput and formatTimeForInput accordingly.
68-93:⚠️ Potential issue | 🟠 MajorAlign status/priority option values with
@forge/constsenums.The select options use values outside
ISSUE_STATUS/PRIORITY(confirmed,tentative, lowercase priorities), and casts at Line 463 mask the mismatch. This can submit values the backend does not accept.Suggested patch
-const STATUS_OPTIONS = [ - { value: "confirmed", label: "Confirmed", caption: "Everything is locked in", dotClass: "bg-emerald-400" }, - { value: "tentative", label: "Tentative", caption: "Waiting on a few details", dotClass: "bg-amber-400" }, - { value: "draft", label: "Draft", caption: "Still being scoped", dotClass: "bg-slate-400" }, - { value: "cancelled", label: "Cancelled", caption: "No longer happening", dotClass: "bg-rose-400" }, -] as const; +const STATUS_OPTIONS: { value: IssueStatus; label: string; caption: string; dotClass: string }[] = [ + { value: "BACKLOG", label: "Backlog", caption: "Not started yet", dotClass: "bg-slate-400" }, + { value: "PLANNING", label: "Planning", caption: "Being scoped", dotClass: "bg-amber-400" }, + { value: "IN_PROGRESS", label: "In Progress", caption: "Actively underway", dotClass: "bg-sky-400" }, + { value: "FINISHED", label: "Finished", caption: "Completed", dotClass: "bg-emerald-400" }, +]; @@ -const PRIORITY_OPTIONS = ["High", "Medium", "Low"]; +const PRIORITY_OPTIONS: IssuePriority[] = [...PRIORITY]; @@ - onValueChange={(value) => - updateForm("status", value as IssueStatus) - } + onValueChange={(value) => updateForm("status", value as IssueStatus)} @@ - {PRIORITY_OPTIONS.map((priority) => ( - <SelectItem - key={priority} - value={priority.toLowerCase()} - > + {PRIORITY_OPTIONS.map((priority) => ( + <SelectItem key={priority} value={priority}> {priority} </SelectItem> ))}#!/bin/bash # Verify enum sources vs local options (read-only). echo "=== Canonical enums ===" sed -n '1,40p' packages/consts/src/issue.ts echo echo "=== Local options and select value wiring ===" sed -n '68,120p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx sed -n '455,705p' apps/blade/src/app/_components/issues/create-edit-dialog.tsxAlso applies to: 110-111, 461-464, 677-693
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 68 - 93, STATUS_OPTIONS (and the Priority options referenced around create-edit-dialog.tsx) use non-canonical string values and then get force-cast when submitting; update the option objects so their value fields match the canonical enums exported from `@forge/consts` (ISSUE_STATUS and PRIORITY) instead of "confirmed"/"tentative" or lowercase priorities, and update any select wiring that currently casts values (see the cast near the submit handler and the SELECT value props around create-edit-dialog.tsx) to rely on the real enum types; in short, replace the value strings in STATUS_OPTIONS (and the priority options) with the corresponding ISSUE_STATUS/PRIORITY enum members and remove the unsafe type casts so submitted payloads use the backend-accepted enum values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Line 927: The submit button copy currently uses intent to render "Create
Event"/"Update Event" but doesn't respect task mode; update the text generation
in the CreateEditDialog component to also consider the task vs event flag (e.g.,
use isTask, mode, or itemType used in this component) and return "Create
Task"/"Update Task" when in task mode and "Create Event"/"Update Event"
otherwise, keeping the existing intent-based switch for create vs update.
---
Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 854-861: The trash-icon-only Button (Button with onClick={() =>
handleRemoveLink(link.id)} rendering <Trash2 />) is missing an accessible name;
add an aria-label (or aria-labelledby) to the Button so screen readers have
context (e.g., aria-label={`Delete link`} or a dynamic label like
aria-label={`Delete link ${link.title || link.url || link.id}`}) while keeping
the existing onClick handler and classes; update the Button that wraps <Trash2
className="h-4 w-4" /> to include this aria attribute so the control is
accessible.
- Around line 938-944: formatDateForInput uses UTC via toISOString() while
formatTimeForInput uses local time via toTimeString(), causing inconsistent
results near timezone boundaries; change both to use the same local-time basis
by deriving date and time from the Date object's local getters (e.g.,
getFullYear, getMonth, getDate for formatDateForInput and getHours, getMinutes
for formatTimeForInput) and zero-pad as needed so both functions consistently
format based on local time; update the implementations of formatDateForInput and
formatTimeForInput accordingly.
- Around line 68-93: STATUS_OPTIONS (and the Priority options referenced around
create-edit-dialog.tsx) use non-canonical string values and then get force-cast
when submitting; update the option objects so their value fields match the
canonical enums exported from `@forge/consts` (ISSUE_STATUS and PRIORITY) instead
of "confirmed"/"tentative" or lowercase priorities, and update any select wiring
that currently casts values (see the cast near the submit handler and the SELECT
value props around create-edit-dialog.tsx) to rely on the real enum types; in
short, replace the value strings in STATUS_OPTIONS (and the priority options)
with the corresponding ISSUE_STATUS/PRIORITY enum members and remove the unsafe
type casts so submitted payloads use the backend-accepted enum values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: dd1ecf33-f938-403d-ae85-54fa8e50ccf6
📒 Files selected for processing (1)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx
| export interface IssueFormValues { | ||
| status: IssueStatus; | ||
| name: string; | ||
| description: string; | ||
| links: LinkItem[]; | ||
| date: string; // ISO string | ||
| priority: IssuePriority; | ||
| team: string; // UUID or name | ||
| parent?: string; | ||
| // UI only fields | ||
| isEvent: boolean; | ||
| // For event, we store event form values | ||
| event?: EventFormValues; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify missing type and key drift inside the dialog file (read-only).
echo "=== IssueFormValues declaration ==="
sed -n '44,90p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== updateForm key usage ==="
rg -nP 'updateForm\("([^"]+)"' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== Direct formValues accesses for UI-only fields ==="
rg -nP 'formValues\.(details|notes|requiresRoom|isHackathonCritical|needsDesignAssets|needsOutreach)' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== LinkItem declaration check ==="
rg -nP '\b(interface|type)\s+LinkItem\b' apps/blade/src/app/_components/issues/create-edit-dialog.tsxRepository: KnightHacks/forge
Length of output: 2150
🏁 Script executed:
#!/bin/bash
echo "=== LinkItem import/type in file ==="
head -50 apps/blade/src/app/_components/issues/create-edit-dialog.tsx | grep -E "import|LinkItem"
echo
echo "=== Search for needsDesignAssets usage ==="
rg -n 'needsDesignAssets' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== Delete button around line 854-861 ==="
sed -n '854,870p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== Check STATUS enum from consts ==="
rg -A10 'export.*STATUS|export.*enum.*Status' packages/consts/src/issue.ts
echo
echo "=== Check PRIORITY enum from consts ==="
rg -A10 'export.*PRIORITY|export.*enum.*Priority' packages/consts/src/issue.tsRepository: KnightHacks/forge
Length of output: 1888
🏁 Script executed:
#!/bin/bash
echo "=== Full imports section (first 80 lines) ==="
sed -n '1,80p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== EventFormValues interface (around 113-127) ==="
sed -n '110,135p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== PRIORITY_OPTIONS definition ==="
rg -B2 -A15 'const PRIORITY_OPTIONS' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== Form submission - what gets sent to API ==="
rg -B5 -A10 'onSubmit\?.*values' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -40Repository: KnightHacks/forge
Length of output: 3943
🏁 Script executed:
#!/bin/bash
echo "=== Search for LinkItem definition across codebase ==="
rg -n 'interface LinkItem|type LinkItem' --type ts --type tsx
echo
echo "=== Submit handler and form value processing ==="
sed -n '900,950p' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== How STATUS_OPTIONS is used in form ==="
rg -B3 -A3 'SelectItem.*value.*confirmed' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -30Repository: KnightHacks/forge
Length of output: 1538
🏁 Script executed:
#!/bin/bash
echo "=== Search for LinkItem definition (no type filter) ==="
rg -n 'LinkItem' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -5
echo
echo "=== Check if LinkItem is in shared types ==="
find . -path ./node_modules -prune -o -name '*.ts' -o -name '*.tsx' | xargs grep -l 'interface LinkItem\|type LinkItem' 2>/dev/null | head -10
echo
echo "=== Check handleSubmit and formValues usage ==="
rg -B5 -A8 'const handleSubmit|form.handleSubmit' apps/blade/src/app/_components/issues/create-edit-dialog.tsx
echo
echo "=== Check the default/initial formValues structure ==="
rg -B2 -A15 'const getDefaultValues|initialValues' apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -50Repository: KnightHacks/forge
Length of output: 2037
Define LinkItem type and sync IssueFormValues with actual form field usage—currently missing required fields causing TypeScript errors and form data loss.
The interface declares links: LinkItem[] without defining LinkItem, and is missing five fields actively bound in the form: requiresRoom, isHackathonCritical, notes, needsDesignAssets, and needsOutreach. Additionally, the form binds to formValues.details but the interface defines description—the submitted data will lack a description field.
Also, fix enum value mismatches: STATUS_OPTIONS values ("confirmed", "tentative", "draft", "cancelled") don't match the imported ISSUE_STATUS constants ("BACKLOG", "PLANNING", "IN_PROGRESS", "FINISHED"), and PRIORITY_OPTIONS (["High", "Medium", "Low"]) don't match PRIORITY (["LOWEST", "LOW", "MEDIUM", "HIGH", "HIGHEST"]). These mismatches will cause API validation failures at runtime.
Finally, the submit button always shows "Create Event" / "Update Event" regardless of isEvent mode, and the delete-link icon button (line 854–861) lacks an accessible name.
Suggested fixes
+ interface LinkItem {
+ id: string;
+ label: string;
+ url: string;
+ }
+
export interface IssueFormValues {
status: IssueStatus;
name: string;
description: string;
links: LinkItem[];
date: string; // ISO string
priority: IssuePriority;
team: string; // UUID or name
parent?: string;
+ requiresRoom: boolean;
+ isHackathonCritical: boolean;
+ notes: string;
+ needsDesignAssets: boolean;
+ needsOutreach: boolean;
// UI only fields
isEvent: boolean;
// For event, we store event form values
event?: EventFormValues;
}At form binding (line 716–719):
- value={formValues.details}
+ value={formValues.description}
onChange={(event) =>
- updateForm("details", event.target.value)
+ updateForm("description", event.target.value)
}For enums, align STATUS_OPTIONS and PRIORITY_OPTIONS with the imported constants rather than hardcoded strings.
Delete button (line 854–861):
<Button
variant="ghost"
type="button"
className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white"
+ aria-label="Remove link"
onClick={() => handleRemoveLink(link.id)}
>Submit button text (line 927):
{intent === "edit"
- ? "Update Event"
- : "Create Event"
+ ? formValues.isEvent ? "Update Event" : "Update Issue"
+ : formValues.isEvent ? "Create Event" : "Create Issue"
}| className="w-full disabled:opacity-40 sm:w-auto" | ||
| disabled={isSubmitDisabled} | ||
| > | ||
| {intent === "edit" ? "Update Event" : "Create Event"} |
There was a problem hiding this comment.
Use task/event-aware submit copy.
Line 927 always renders event text, which is incorrect in task mode.
Suggested patch
- {intent === "edit" ? "Update Event" : "Create Event"}
+ {intent === "edit"
+ ? formValues.isEvent
+ ? "Update Event"
+ : "Update Task"
+ : formValues.isEvent
+ ? "Create Event"
+ : "Create Task"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` at line 927,
The submit button copy currently uses intent to render "Create Event"/"Update
Event" but doesn't respect task mode; update the text generation in the
CreateEditDialog component to also consider the task vs event flag (e.g., use
isTask, mode, or itemType used in this component) and return "Create
Task"/"Update Task" when in task mode and "Create Event"/"Update Event"
otherwise, keeping the existing intent-based switch for create vs update.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (1)
534-553:⚠️ Potential issue | 🟠 MajorUse the enum value as the select value.
formValues.prioritystoresISSUE.PRIORITYmembers like"LOWEST", but eachSelectItempublishespriority.toLowerCase(). After the first selection, the state contains a value outsideIssuePriority, the current option no longer matches, and any upstream enum validation will fail. Keep thevalueuppercase and only format the label for display.Suggested patch
{ISSUE.PRIORITY.map((priority) => ( - <SelectItem key={priority} value={priority.toLowerCase()}> - {priority} + <SelectItem key={priority} value={priority}> + {getStatusLabel(priority)} </SelectItem> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 534 - 553, The Select is storing lowercase strings (priority.toLowerCase()) while formValues.priority expects ISSUE.PRIORITY enum members (e.g., "LOWEST"), causing mismatches; update the SelectItem value to use the enum member (priority) instead of priority.toLowerCase(), keep the displayed label formatted (e.g., render priority.toLowerCase() or humanize it inside the SelectItem children) and ensure updateForm("priority", value) continues to receive the uppercase enum value so formValues.priority and any upstream IssuePriority validation remain consistent.packages/consts/src/issue.ts (1)
39-50:⚠️ Potential issue | 🔴 CriticalSync
IssueFormValueswith what the dialog actually edits.
CreateEditDialogreads/writesdetails,notes,requiresRoom,isHackathonCritical,needsDesignAssets, andneedsOutreachinapps/blade/src/app/_components/issues/create-edit-dialog.tsx(for example Lines 574-576, 594-596, 616-618, and 718-720), but those keys do not exist here. BecauseupdateFormis keyed off this interface, the component will not type-check, and the currentdetailsbinding also means the dialog never populates the existingdescriptionfield. Either add the missing state keys here or rename the component bindings to the existing schema before merge.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/consts/src/issue.ts` around lines 39 - 50, The IssueFormValues interface is missing fields that CreateEditDialog reads/writes (details, notes, requiresRoom, isHackathonCritical, needsDesignAssets, needsOutreach) causing type errors and incorrect binding of description; update the IssueFormValues interface to include these properties with appropriate types (e.g., details: string, notes: string, requiresRoom: boolean, isHackathonCritical: boolean, needsDesignAssets: boolean, needsOutreach: boolean and keep existing fields like description) so that updateForm and CreateEditDialog bindings compile and the dialog correctly populates description, or alternatively change CreateEditDialog’s bindings to the existing schema (e.g., use description instead of details) but be sure to update all references in CreateEditDialog and any callers (including updateForm) to match the chosen schema.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 53-70: The event form currently leaves required fields tag and
description empty because defaultEventForm sets them but the Event branch UI (in
create-edit-dialog.tsx) never renders inputs for them and handleSubmit forwards
formValues unchanged to onSubmit; update the Event branch to either (a) render
controlled inputs for tag and description and validate them before submission
(modify the JSX where event mode is handled to include these fields and their
handlers) or (b) if you intend defaults, populate formValues.tag and
formValues.description with sensible non-empty values just prior to calling
handleSubmit/onSubmit (update the handleSubmit path that prepares the payload),
and add client-side validation in the submit flow to reject empty
tag/description so the required server schema is satisfied (referencing
defaultEventForm, handleSubmit, and onSubmit in this file).
In `@packages/consts/src/issue.ts`:
- Around line 52-65: EventFormValues currently requires discordId and googleId
even though the dialog never edits them and createEvent omits them; update the
form model by making discordId and googleId optional (discordId?: string;
googleId?: string) or removing them from EventFormValues and defaultEventForm
entirely, and adjust any usages (e.g., defaultEventForm and form initialization)
so they don't fabricate empty strings; also ensure the API shape in createEvent
(packages/api/src/routers/event.ts, createEvent handler) remains consistent with
the revised EventFormValues so there is no mismatch between the form type and
the create payload.
---
Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 534-553: The Select is storing lowercase strings
(priority.toLowerCase()) while formValues.priority expects ISSUE.PRIORITY enum
members (e.g., "LOWEST"), causing mismatches; update the SelectItem value to use
the enum member (priority) instead of priority.toLowerCase(), keep the displayed
label formatted (e.g., render priority.toLowerCase() or humanize it inside the
SelectItem children) and ensure updateForm("priority", value) continues to
receive the uppercase enum value so formValues.priority and any upstream
IssuePriority validation remain consistent.
In `@packages/consts/src/issue.ts`:
- Around line 39-50: The IssueFormValues interface is missing fields that
CreateEditDialog reads/writes (details, notes, requiresRoom,
isHackathonCritical, needsDesignAssets, needsOutreach) causing type errors and
incorrect binding of description; update the IssueFormValues interface to
include these properties with appropriate types (e.g., details: string, notes:
string, requiresRoom: boolean, isHackathonCritical: boolean, needsDesignAssets:
boolean, needsOutreach: boolean and keep existing fields like description) so
that updateForm and CreateEditDialog bindings compile and the dialog correctly
populates description, or alternatively change CreateEditDialog’s bindings to
the existing schema (e.g., use description instead of details) but be sure to
update all references in CreateEditDialog and any callers (including updateForm)
to match the chosen schema.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 7d55970f-f0d2-4c1d-b439-169511cd1df2
📒 Files selected for processing (2)
apps/blade/src/app/_components/issues/create-edit-dialog.tsxpackages/consts/src/issue.ts
| const defaultEventForm = (): ISSUE.EventFormValues => { | ||
| const now = new Date(); | ||
| const end = new Date(now.getTime() + 60 * 60 * 1000); | ||
| return { | ||
| discordId: "", | ||
| googleId: "", | ||
| name: "", | ||
| tag: "", | ||
| description: "", | ||
| startDate: formatDateForInput(now), | ||
| startTime: formatTimeForInput(now), | ||
| endDate: formatDateForInput(end), | ||
| endTime: formatTimeForInput(end), | ||
| location: "", | ||
| dues_paying: false, | ||
| points: undefined, | ||
| hackathonId: undefined, | ||
| }; |
There was a problem hiding this comment.
Event mode never collects a complete event payload.
defaultEventForm initializes tag and description, but the event branch only renders name/date/time/location controls, and handleSubmit forwards formValues unchanged. That means a new event can reach onSubmit with event.tag === "", and event.description is never updated anywhere in this component even though the Event schema/API treat both fields as required in packages/db/src/schemas/knight-hacks.ts:165-189 and packages/api/src/routers/event.ts:168-200. Add event-specific inputs for these fields or populate them before calling onSubmit.
As per coding guidelines apps/blade/**: Form validation on both client and server.
Also applies to: 224-231, 370-459
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 53
- 70, The event form currently leaves required fields tag and description empty
because defaultEventForm sets them but the Event branch UI (in
create-edit-dialog.tsx) never renders inputs for them and handleSubmit forwards
formValues unchanged to onSubmit; update the Event branch to either (a) render
controlled inputs for tag and description and validate them before submission
(modify the JSX where event mode is handled to include these fields and their
handlers) or (b) if you intend defaults, populate formValues.tag and
formValues.description with sensible non-empty values just prior to calling
handleSubmit/onSubmit (update the handleSubmit path that prepares the payload),
and add client-side validation in the submit flow to reject empty
tag/description so the required server schema is satisfied (referencing
defaultEventForm, handleSubmit, and onSubmit in this file).
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (4)
692-699:⚠️ Potential issue | 🟡 MinorAdd
aria-labelto the delete link button for accessibility.The button only contains an icon (
Trash2). Screen readers will announce "button" without context.Fix: Add accessible label
<Button variant="ghost" type="button" className="h-11 w-11 rounded-2xl border border-white/10 bg-transparent text-white/70 hover:border-white/30 hover:text-white" onClick={() => handleRemoveLink(i)} + aria-label="Remove link" > <Trash2 className="h-4 w-4" /> </Button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 692 - 699, The delete button that renders the Trash2 icon lacks an accessible label; update the Button rendered in create-edit-dialog.tsx (the Button with onClick={() => handleRemoveLink(i)}) to include an aria-label (for example aria-label={`Remove link ${i + 1}`} or a static "Remove link") so screen readers receive context about the button's purpose while keeping the existing onClick and styling.
774-780:⚠️ Potential issue | 🟡 MinorTimezone inconsistency between date and time formatting.
formatDateForInputusestoISOString()(UTC), butformatTimeForInputusestoTimeString()(local). Near midnight, this can cause the displayed date to be off by one day.Example: At 11 PM in UTC-5,
toISOString()returns tomorrow's date whiletoTimeString()returns today's time.Fix: Use consistent local time formatting
function formatDateForInput(date: Date) { - return date.toISOString().slice(0, 10); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 774 - 780, The date/time formatting is inconsistent: formatDateForInput uses UTC via toISOString() while formatTimeForInput uses local time via toTimeString(), which can produce off-by-one-day dates near midnight; update formatDateForInput to use local date fields (e.g., getFullYear(), getMonth()+1, getDate()) and zero-pad to produce a "YYYY-MM-DD" string consistent with formatTimeForInput (or alternatively make both use toLocaleDateString('en-CA') and a local time formatter) so both functions operate in the same timezone (functions: formatDateForInput and formatTimeForInput).
758-764:⚠️ Potential issue | 🟡 MinorSubmit button always shows "Event" text regardless of mode.
Line 763 hardcodes "Event" but should adapt based on
formValues.isEvent.Fix: Use task/event-aware copy
<Button type="submit" className="w-full disabled:opacity-40 sm:w-auto" disabled={isSubmitDisabled} > - {intent === "edit" ? "Update Event" : "Create Event"} + {intent === "edit" + ? formValues.isEvent ? "Update Event" : "Update Task" + : formValues.isEvent ? "Create Event" : "Create Task"} </Button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 758 - 764, The submit button currently hardcodes "Event" in its label; update the Button render (inside create-edit-dialog.tsx) to pick "Task" vs "Event" based on formValues.isEvent and still respect intent. Replace the current label expression ({intent === "edit" ? "Update Event" : "Create Event"}) with a conditional that uses formValues.isEvent to produce "Update Task"/"Create Task" when false and "Update Event"/"Create Event" when true, referencing the existing intent and formValues.isEvent variables so the button text adapts to both mode and type.
71-89:⚠️ Potential issue | 🟠 MajorEvent mode lacks inputs for required
taganddescriptionfields.The
defaultEventForm()initializestaganddescriptionto empty strings, but the event UI branch (lines 372-459) doesn't render inputs for these fields. PerEventFormValues, both are required—submitting with empty values will fail server validation.Why it matters: The API expects non-empty
taganddescriptionfor events. Users have no way to fill these fields.Suggested fix: Add inputs for tag and description in event mode
Add these fields inside the event date/time section or in the Details tab when
isEventis true:+ {formValues.isEvent && ( + <div className="space-y-4"> + <div className="space-y-2"> + <Label className="text-sm text-white/70">Event Tag</Label> + <Input + className={cn(baseField, "h-11")} + placeholder="e.g., workshop, social, hackathon" + value={formValues.event?.tag ?? ""} + onChange={(e) => updateEventForm("tag", e.target.value)} + /> + </div> + <div className="space-y-2"> + <Label className="text-sm text-white/70">Event Description</Label> + <Textarea + className={cn(baseField, "min-h-[100px] resize-none")} + placeholder="Describe the event..." + value={formValues.event?.description ?? ""} + onChange={(e) => updateEventForm("description", e.target.value)} + /> + </div> + </div> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 71 - 89, defaultEventForm() sets required EventFormValues.tag and .description to empty strings but the event UI branch (the isEvent branch in create-edit-dialog.tsx that renders the event date/time and details) does not render inputs for these fields; add form inputs for "tag" and "description" to the event mode UI (either inside the event date/time section or the Details tab shown when isEvent is true) and wire them to the same form state handlers used elsewhere (the form field names must match EventFormValues.tag and EventFormValues.description and use the existing onChange/onBlur or form register functions so server validation can receive non-empty values on submit).
🧹 Nitpick comments (1)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (1)
677-680: Consider stable keys for link items.Using array index as
key(line 679) can cause React to misassociate DOM state when items are removed mid-list. Since links are simple strings, you could generate stable IDs when adding items.Why it matters: If a user deletes link 2 of 3, the input for link 3 might retain link 2's focus/cursor state.
Optional: Use stable IDs
- const createLinkItem = (): string => ""; + interface LinkEntry { + id: string; + url: string; + } + const createLinkItem = (): LinkEntry => ({ id: crypto.randomUUID(), url: "" });Then update the links array type and map by
link.id.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 677 - 680, The map over formValues.links is using the array index as key (key={i}) which can break React reconciliation; change the links items to objects with a stable id and value (e.g., {id: string, url: string}) when creating/initializing links, generate the id in the add-link handler (use nanoid/uuid or a simple incrementer), update all consumers (the form state initialization, onChange handler, and remove handler) to read/write link.url and link.id, and then map using key={link.id} instead of the index so inputs keep stable identity when items are inserted/removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 547-557: The SelectItem values use priority.toLowerCase(), which
mismatches ISSUE.PRIORITY and formValues.priority (uppercase) and breaks
selection and onValueChange typing; update the SelectItem value to use the
original priority casing (value={priority}) or alternatively normalize values
consistently by converting incoming onValueChange to the enum casing inside the
handler (update the handler used by onValueChange in the create/edit dialog),
ensuring SelectItem, formValues.priority and onValueChange all use the same enum
casing (reference SelectItem, SelectContent, ISSUE.PRIORITY,
formValues.priority, and the onValueChange handler).
---
Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 692-699: The delete button that renders the Trash2 icon lacks an
accessible label; update the Button rendered in create-edit-dialog.tsx (the
Button with onClick={() => handleRemoveLink(i)}) to include an aria-label (for
example aria-label={`Remove link ${i + 1}`} or a static "Remove link") so screen
readers receive context about the button's purpose while keeping the existing
onClick and styling.
- Around line 774-780: The date/time formatting is inconsistent:
formatDateForInput uses UTC via toISOString() while formatTimeForInput uses
local time via toTimeString(), which can produce off-by-one-day dates near
midnight; update formatDateForInput to use local date fields (e.g.,
getFullYear(), getMonth()+1, getDate()) and zero-pad to produce a "YYYY-MM-DD"
string consistent with formatTimeForInput (or alternatively make both use
toLocaleDateString('en-CA') and a local time formatter) so both functions
operate in the same timezone (functions: formatDateForInput and
formatTimeForInput).
- Around line 758-764: The submit button currently hardcodes "Event" in its
label; update the Button render (inside create-edit-dialog.tsx) to pick "Task"
vs "Event" based on formValues.isEvent and still respect intent. Replace the
current label expression ({intent === "edit" ? "Update Event" : "Create Event"})
with a conditional that uses formValues.isEvent to produce "Update Task"/"Create
Task" when false and "Update Event"/"Create Event" when true, referencing the
existing intent and formValues.isEvent variables so the button text adapts to
both mode and type.
- Around line 71-89: defaultEventForm() sets required EventFormValues.tag and
.description to empty strings but the event UI branch (the isEvent branch in
create-edit-dialog.tsx that renders the event date/time and details) does not
render inputs for these fields; add form inputs for "tag" and "description" to
the event mode UI (either inside the event date/time section or the Details tab
shown when isEvent is true) and wire them to the same form state handlers used
elsewhere (the form field names must match EventFormValues.tag and
EventFormValues.description and use the existing onChange/onBlur or form
register functions so server validation can receive non-empty values on submit).
---
Nitpick comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 677-680: The map over formValues.links is using the array index as
key (key={i}) which can break React reconciliation; change the links items to
objects with a stable id and value (e.g., {id: string, url: string}) when
creating/initializing links, generate the id in the add-link handler (use
nanoid/uuid or a simple incrementer), update all consumers (the form state
initialization, onChange handler, and remove handler) to read/write link.url and
link.id, and then map using key={link.id} instead of the index so inputs keep
stable identity when items are inserted/removed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: e745c88b-6e8c-4626-bbda-a945a5324001
📒 Files selected for processing (2)
apps/blade/src/app/_components/issues/create-edit-dialog.tsxpackages/consts/src/issue.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/consts/src/issue.ts
| <SelectContent> | ||
| {ISSUE.PRIORITY.map((priority) => ( | ||
| <SelectItem | ||
| key={priority} | ||
| value={priority.toLowerCase()} | ||
| > | ||
| {priority} | ||
| </SelectItem> | ||
| ))} | ||
| </SelectContent> | ||
| </Select> |
There was a problem hiding this comment.
Priority select value casing mismatch will break selection state.
SelectItem values are set to priority.toLowerCase() (line 551), but formValues.priority stores uppercase values from ISSUE.PRIORITY. This causes two problems:
- The current selection won't display (values don't match)
onValueChangereceives lowercase but the type expects uppercase
Fix: Keep values consistent with the enum:
Proposed fix
<SelectContent>
{ISSUE.PRIORITY.map((priority) => (
<SelectItem
key={priority}
- value={priority.toLowerCase()}
+ value={priority}
>
{priority}
</SelectItem>
))}
</SelectContent>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
547 - 557, The SelectItem values use priority.toLowerCase(), which mismatches
ISSUE.PRIORITY and formValues.priority (uppercase) and breaks selection and
onValueChange typing; update the SelectItem value to use the original priority
casing (value={priority}) or alternatively normalize values consistently by
converting incoming onValueChange to the enum casing inside the handler (update
the handler used by onValueChange in the create/edit dialog), ensuring
SelectItem, formValues.priority and onValueChange all use the same enum casing
(reference SelectItem, SelectContent, ISSUE.PRIORITY, formValues.priority, and
the onValueChange handler).
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx (3)
534-551:⚠️ Potential issue | 🟠 MajorKeep priority option values in enum casing.
formValues.prioritystoresISSUE.PRIORITYentries, but eachSelectItemlowercases itsvalue. That breaks the selected state and feeds non-enum strings back throughonValueChange.Suggested patch
{ISSUE.PRIORITY.map((priority) => ( <SelectItem key={priority} - value={priority.toLowerCase()} + value={priority} > {priority} </SelectItem> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 534 - 551, The SelectItems are lowercasing values, causing mismatch with the stored enum entries in formValues.priority and breaking selection; change each SelectItem to use the original enum casing (use value={priority} instead of value={priority.toLowerCase()}) and ensure the Select onValueChange still casts to (typeof ISSUE.PRIORITY)[number] in updateForm so the component stores and emits the exact ISSUE.PRIORITY enum values; locate SelectItem, SelectTrigger, the onValueChange handler, updateForm call, and ISSUE.PRIORITY to make this change.
560-577:⚠️ Potential issue | 🟠 MajorPersist the textarea into
description.This field is labeled “Description”, but it only mutates
formValues.details.formValues.descriptionstays at the default"", so any downstream code reading the canonical field loses the user’s text.Suggested patch
- value={formValues.details} + value={formValues.description} onChange={(event) => - updateForm("details", event.target.value) + updateForm("description", event.target.value) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 560 - 577, The Description textarea is currently bound to formValues.details so formValues.description remains empty; change the binding and update to use the canonical field (use value={formValues.description} and call updateForm("description", event.target.value) in the Textarea's onChange) so the Label/Textarea pair (identified by baseId, Label, Textarea, formValues.details/formValues.description, updateForm) persists the user's input into formValues.description for downstream consumers.
70-87:⚠️ Potential issue | 🟠 MajorEvent mode still submits blank required fields.
taganddescriptionstart as"", the event branch never edits them, and the submit gate only checksnamebefore passingformValuesthrough unchanged. SinceISSUE.EventFormValuesstill requires both fields, create-event flows can pass client-side and still send an invalid payload. Either collect those fields in the event UI or hydrateformValues.eventfrom existing dialog inputs insidehandleSubmitbefore callingonSubmit.As per coding guidelines
apps/blade/**: Form validation on both client and server.Also applies to: 153-155, 246-253, 370-459
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines 70 - 87, The event creation flow allows submitting blank required fields because defaultEventForm sets tag and description to empty strings and the event branch never updates them; modify handleSubmit to read the dialog's event-specific inputs (tag and description) and hydrate formValues.event (ISSUE.EventFormValues) with those values before calling onSubmit, and ensure the submit gate validates that formValues.name, formValues.event.tag, and formValues.event.description are non-empty so invalid payloads are blocked client-side; reference defaultEventForm, handleSubmit, onSubmit and formValues when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 184-205: The dialog effect that currently handles scroll locking
and Escape (inside the React.useEffect using previousOverflow and handleKeydown)
must also manage focus for accessibility: capture and store
document.activeElement before opening, move focus into the dialog container when
open, set role="dialog" with aria-modal="true" and a programmatic name via
aria-labelledby or aria-label on the dialog element, ensure the first name input
has an explicit id and its label uses htmlFor so it serves as the dialog's
labelledby target, restore focus to the previously focused element on close (in
the effect cleanup), and add a small keyboard focus trap inside the dialog to
keep Tab/Shift+Tab cycling within the dialog while open; update the dialog
container element and the useEffect cleanup to perform these focus/restore/trap
responsibilities alongside the existing overflow/keydown logic.
- Around line 468-478: The date picker is using new Date(e.target.value) (which
parses YYYY-MM-DD as UTC) and then setHours in local time, causing day shifts;
change the onChange for the Input with id `${baseId}-task-due-date` to parse
e.target.value by splitting into year, month, day and construct the Date via new
Date(year, month-1, day, 23, 0, 0, 0) before calling updateForm("date",
d.toISOString()); also replace the value expression that uses
formValues.date.slice(0,10) with a local-time formatter (or add a helper like
formatDateForInput(formValues.date)) that returns the YYYY-MM-DD string in local
time so the input and stored ISO string remain consistent.
- Around line 130-148: The form is being reset whenever buildInitialFormValues
(which depends on the entire initialValues object) changes, causing unwanted
resets when the parent recreates initialValues; modify the effect that calls
reset to only run when the dialog transitions from closed→open by tracking
previous open state with a ref (e.g., wasOpenRef) instead of depending on
buildInitialFormValues, and if the dialog can switch entities while open also
include a stable entity identifier (like initialValues.id) in the open
transition check; keep buildInitialFormValues as-is for computing values but
remove it from the reset effect dependencies and use the ref-based open
transition to trigger reset.
---
Duplicate comments:
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx`:
- Around line 534-551: The SelectItems are lowercasing values, causing mismatch
with the stored enum entries in formValues.priority and breaking selection;
change each SelectItem to use the original enum casing (use value={priority}
instead of value={priority.toLowerCase()}) and ensure the Select onValueChange
still casts to (typeof ISSUE.PRIORITY)[number] in updateForm so the component
stores and emits the exact ISSUE.PRIORITY enum values; locate SelectItem,
SelectTrigger, the onValueChange handler, updateForm call, and ISSUE.PRIORITY to
make this change.
- Around line 560-577: The Description textarea is currently bound to
formValues.details so formValues.description remains empty; change the binding
and update to use the canonical field (use value={formValues.description} and
call updateForm("description", event.target.value) in the Textarea's onChange)
so the Label/Textarea pair (identified by baseId, Label, Textarea,
formValues.details/formValues.description, updateForm) persists the user's input
into formValues.description for downstream consumers.
- Around line 70-87: The event creation flow allows submitting blank required
fields because defaultEventForm sets tag and description to empty strings and
the event branch never updates them; modify handleSubmit to read the dialog's
event-specific inputs (tag and description) and hydrate formValues.event
(ISSUE.EventFormValues) with those values before calling onSubmit, and ensure
the submit gate validates that formValues.name, formValues.event.tag, and
formValues.event.description are non-empty so invalid payloads are blocked
client-side; reference defaultEventForm, handleSubmit, onSubmit and formValues
when making this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 8bfbe032-f578-46ff-883e-d128672e93b5
📒 Files selected for processing (1)
apps/blade/src/app/_components/issues/create-edit-dialog.tsx
| const buildInitialFormValues = React.useCallback(() => { | ||
| const defaults = defaultForm(); | ||
| if (initialValues?.isEvent) { | ||
| return { | ||
| ...defaults, | ||
| ...initialValues, | ||
| isEvent: true, | ||
| event: initialValues.event ?? defaultEventForm(), | ||
| links: initialValues?.links ?? defaults.links, | ||
| }; | ||
| } | ||
| return { | ||
| ...defaults, | ||
| ...initialValues, | ||
| isEvent: false, | ||
| event: undefined, | ||
| links: initialValues?.links ?? defaults.links, | ||
| }; | ||
| }, [initialValues]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "create-edit-dialog.tsx" --type fRepository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | head -200Repository: KnightHacks/forge
Length of output: 6321
🏁 Script executed:
wc -l "apps/blade/src/app/_components/issues/create-edit-dialog.tsx"Repository: KnightHacks/forge
Length of output: 125
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '175,220p'Repository: KnightHacks/forge
Length of output: 1456
🏁 Script executed:
rg "CreateEditDialog" --type tsx -B 2 -A 5 apps/blade/src/appRepository: KnightHacks/forge
Length of output: 88
🏁 Script executed:
rg "initialValues" apps/blade/src/app/_components/issues/ --type tsx -B 3 -A 3Repository: KnightHacks/forge
Length of output: 88
🏁 Script executed:
rg "CreateEditDialog" -A 5 apps/blade/src/app/Repository: KnightHacks/forge
Length of output: 572
🏁 Script executed:
rg "initialValues" apps/blade/src/app/_components/issues/ -B 3 -A 3Repository: KnightHacks/forge
Length of output: 2688
🏁 Script executed:
fd "page.tsx" apps/blade/src/app | head -20Repository: KnightHacks/forge
Length of output: 1138
🏁 Script executed:
rg "CreateEditDialog" --type js --type ts -l apps/blade/src/appRepository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
grep -r "CreateEditDialog" apps/blade/src/app --include="*.tsx" --include="*.ts" -lRepository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
rg "CreateEditDialog" --type js --type ts -B 2 -A 8Repository: KnightHacks/forge
Length of output: 1509
🏁 Script executed:
find apps/blade/src -name "*.tsx" -o -name "*.ts" | xargs grep -l "CreateEditDialog" 2>/dev/nullRepository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
rg "import.*CreateEditDialog" -A 5 -B 2Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
find apps/blade -name "*.tsx" -exec grep -l "CreateEditDialog" {} \;Repository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '1,160p'Repository: KnightHacks/forge
Length of output: 5227
🏁 Script executed:
rg "issues" apps/blade/src/app --include="*.tsx" -l | head -10Repository: KnightHacks/forge
Length of output: 502
🏁 Script executed:
find apps/blade/src/app -type f \( -name "*.tsx" -o -name "*.ts" \) | xargs grep -l "issue" | head -10Repository: KnightHacks/forge
Length of output: 321
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | tail -100Repository: KnightHacks/forge
Length of output: 4149
🏁 Script executed:
cat -n "apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx" | head -100Repository: KnightHacks/forge
Length of output: 3761
🏁 Script executed:
rg "initialValues" apps/blade/src/app/_components/dashboard/ -B 3 -A 3Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
grep -r "CreateEditDialog" apps/blade/src/app --include="*.tsx" -B 5 -A 10Repository: KnightHacks/forge
Length of output: 1365
🏁 Script executed:
find apps/blade -name "*.tsx" -exec grep -l "<CreateEditDialog" {} \;Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
grep -r "CreateEditDialog" . --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v node_modulesRepository: KnightHacks/forge
Length of output: 266
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '140,185p'Repository: KnightHacks/forge
Length of output: 1441
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '128,182p'Repository: KnightHacks/forge
Length of output: 1895
🏁 Script executed:
rg "open.*initialValues|initialValues.*open" apps/blade/src/app -B 3 -A 3Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
git log --all --oneline --decorate -- "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | head -10Repository: KnightHacks/forge
Length of output: 117
🏁 Script executed:
grep -r "CreateEditDialog\|create-edit-dialog" apps/blade --include="*.tsx" -lRepository: KnightHacks/forge
Length of output: 121
🏁 Script executed:
fd "admin" apps/blade/src/app --type d | xargs grep -l "issue" 2>/dev/null | head -5Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
find apps/blade/src/app -type f -name "*.tsx" | xargs grep -l "issue\|Issue" | head -10Repository: KnightHacks/forge
Length of output: 619
🏁 Script executed:
rg "from.*create-edit-dialog\|import.*CreateEditDialog" apps/blade --type ts --type tsxRepository: KnightHacks/forge
Length of output: 88
🏁 Script executed:
find . -name "*.tsx" -o -name "*.ts" | xargs grep "CreateEditDialog" 2>/dev/nullRepository: KnightHacks/forge
Length of output: 266
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '148,182p'Repository: KnightHacks/forge
Length of output: 1152
🏁 Script executed:
cat -n "apps/blade/src/app/_components/issues/create-edit-dialog.tsx" | sed -n '116,150p'Repository: KnightHacks/forge
Length of output: 1314
🏁 Script executed:
ast-grep --pattern 'const buildInitialFormValues = React.useCallback'Repository: KnightHacks/forge
Length of output: 43
Reset form only when dialog opens, not when initialValues object identity changes.
Lines 175-182: The reset effect depends on buildInitialFormValues (line 182), which depends on the whole initialValues object (line 148). If the parent recreates that object while the dialog is open—even with the same data—this triggers a form reset and snaps the user back to Details section, wiping in-progress edits.
Use a ref to track the open→closed transition instead:
Suggested patch
+ const previousOpenRef = React.useRef(false);
+
React.useEffect(() => {
- if (!open) {
- return;
+ const justOpened = open && !previousOpenRef.current;
+ if (justOpened) {
+ setFormValues(buildInitialFormValues());
+ setActiveSection("details");
}
-
- setFormValues(buildInitialFormValues());
- setActiveSection("details");
+ previousOpenRef.current = open;
}, [buildInitialFormValues, open]);If this dialog can switch between different entities while open, include a stable entity key (e.g., ID) in the condition as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
130 - 148, The form is being reset whenever buildInitialFormValues (which
depends on the entire initialValues object) changes, causing unwanted resets
when the parent recreates initialValues; modify the effect that calls reset to
only run when the dialog transitions from closed→open by tracking previous open
state with a ref (e.g., wasOpenRef) instead of depending on
buildInitialFormValues, and if the dialog can switch entities while open also
include a stable entity identifier (like initialValues.id) in the open
transition check; keep buildInitialFormValues as-is for computing values but
remove it from the reset effect dependencies and use the ref-based open
transition to trigger reset.
| React.useEffect(() => { | ||
| if (!open) { | ||
| return; | ||
| } | ||
|
|
||
| const previousOverflow = document.body.style.overflow; | ||
| document.body.style.overflow = "hidden"; | ||
|
|
||
| const handleKeydown = (event: KeyboardEvent) => { | ||
| if (event.key === "Escape") { | ||
| event.preventDefault(); | ||
| onClose?.(); | ||
| } | ||
| }; | ||
|
|
||
| window.addEventListener("keydown", handleKeydown); | ||
|
|
||
| return () => { | ||
| document.body.style.overflow = previousOverflow; | ||
| window.removeEventListener("keydown", handleKeydown); | ||
| }; | ||
| }, [open, onClose]); |
There was a problem hiding this comment.
Finish the custom dialog accessibility contract.
This portal locks scrolling and handles Escape, but it still never moves focus into the modal, restores focus on close, or gives role="dialog" a programmatic name. The first name field is also visually labeled only, so assistive tech lands in an unnamed dialog with an unlabeled primary input.
Suggested patch
+ const closeButtonRef = React.useRef<HTMLButtonElement>(null);
+ const previousActiveElementRef = React.useRef<HTMLElement | null>(null);
+
React.useEffect(() => {
if (!open) {
return;
}
+ previousActiveElementRef.current =
+ document.activeElement instanceof HTMLElement
+ ? document.activeElement
+ : null;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
+ closeButtonRef.current?.focus();
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
@@
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", handleKeydown);
+ previousActiveElementRef.current?.focus();
};
}, [open, onClose]);
@@
<div
aria-modal
role="dialog"
+ aria-labelledby={`${baseId}-dialog-title`}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4 py-10"
onMouseDown={handleOverlayPointerDown}
>
@@
- <button
+ <button
+ ref={closeButtonRef}
type="button"
className="absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md border border-input text-muted-foreground transition hover:text-foreground"
onClick={onClose}
@@
- <h2 className="mt-1 text-lg font-semibold">
+ <h2 id={`${baseId}-dialog-title`} className="mt-1 text-lg font-semibold">
@@
- <Label className="text-right">
+ <Label htmlFor={`${baseId}-name`} className="text-right">
{formValues.isEvent ? "Event Name" : "Task Name"}
</Label>
<Input
+ id={`${baseId}-name`}
className={cn(baseField, "col-span-3")}You’ll still want a small focus trap here so Tab cannot escape behind the overlay.
As per coding guidelines apps/blade/**: Accessibility (alt text, ARIA, semantic HTML).
Also applies to: 266-329
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
184 - 205, The dialog effect that currently handles scroll locking and Escape
(inside the React.useEffect using previousOverflow and handleKeydown) must also
manage focus for accessibility: capture and store document.activeElement before
opening, move focus into the dialog container when open, set role="dialog" with
aria-modal="true" and a programmatic name via aria-labelledby or aria-label on
the dialog element, ensure the first name input has an explicit id and its label
uses htmlFor so it serves as the dialog's labelledby target, restore focus to
the previously focused element on close (in the effect cleanup), and add a small
keyboard focus trap inside the dialog to keep Tab/Shift+Tab cycling within the
dialog while open; update the dialog container element and the useEffect cleanup
to perform these focus/restore/trap responsibilities alongside the existing
overflow/keydown logic.
| <Input | ||
| id={`${baseId}-task-due-date`} | ||
| type="date" | ||
| className={cn(baseField, "col-span-3")} | ||
| value={formValues.date.slice(0, 10)} | ||
| onChange={(e) => { | ||
| // Always set to 11:00 PM | ||
| const d = new Date(e.target.value); | ||
| d.setHours(23, 0, 0, 0); | ||
| updateForm("date", d.toISOString()); | ||
| }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | head -500 | tail -100Repository: KnightHacks/forge
Length of output: 4538
🏁 Script executed:
cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '460,490p'Repository: KnightHacks/forge
Length of output: 1406
🏁 Script executed:
rg "formValues\.date" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 2 -A 2Repository: KnightHacks/forge
Length of output: 286
🏁 Script executed:
cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '1,100p'Repository: KnightHacks/forge
Length of output: 3242
🏁 Script executed:
rg "formValues\s*=" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 3 -A 3Repository: KnightHacks/forge
Length of output: 43
🏁 Script executed:
rg "updateForm" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 1 -A 1 | head -50Repository: KnightHacks/forge
Length of output: 1589
🏁 Script executed:
rg "formatDateForInput|formatTimeForInput" apps/blade/src/app/_components/issues/create-edit-dialog.tsx -B 2 -A 5Repository: KnightHacks/forge
Length of output: 527
🏁 Script executed:
cat -n apps/blade/src/app/_components/issues/create-edit-dialog.tsx | sed -n '100,150p'Repository: KnightHacks/forge
Length of output: 1725
🏁 Script executed:
python3 << 'EOF'
# Verify JavaScript Date parsing behavior
from datetime import datetime, timezone
# Demonstrate the issue:
# When you parse "2025-03-15" with new Date() in JavaScript, it's treated as UTC midnight
# When you set local hours to 23 with setHours(23, 0, 0, 0), it depends on timezone
print("=== JavaScript Date Behavior Analysis ===\n")
print("1. String '2025-03-15' parsed with new Date():")
print(" - Treated as UTC midnight: 2025-03-15T00:00:00Z")
print(" - This is timezone-dependent interpretation\n")
print("2. When setHours(23, 0, 0, 0) is called:")
print(" - Sets LOCAL hours to 23")
print(" - In EST (UTC-5): 23:00 local = 04:00 UTC next day")
print(" - In UTC: 23:00 local = 23:00 UTC same day")
print(" - In JST (UTC+9): 23:00 local = 14:00 UTC same day\n")
print("3. The rendering issue:")
print(" - formValues.date.slice(0, 10) extracts UTC date portion")
print(" - But user's input picker shows local calendar")
print(" - Mismatch: stored as UTC, displayed as if it were local\n")
print("Example scenario (EST timezone):")
print("- User wants to set task for March 15")
print("- Stored value: 2025-03-16T04:00:00Z (11 PM EST = 4 AM UTC)")
print("- Displayed: 2025-03-16 (wrong day!)")
print("- When edited, the day can shift again")
EOFRepository: KnightHacks/forge
Length of output: 875
🏁 Script executed:
rg "getFullYear|getMonth|getDate" apps/blade/src --type ts --type tsx -lRepository: KnightHacks/forge
Length of output: 88
🏁 Script executed:
cat CONTRIBUTING.md 2>/dev/null | grep -i "date\|time\|timezone" -A 3 -B 1 || echo "No date/time guidance in CONTRIBUTING.md"Repository: KnightHacks/forge
Length of output: 364
🏁 Script executed:
rg "date-fns|dayjs|luxon" apps/blade/package.jsonRepository: KnightHacks/forge
Length of output: 43
Fix date picker to work with local calendar, not UTC.
Parsing YYYY-MM-DD with new Date() treats it as UTC midnight, but setHours(23) operates in local time. This creates a day shift: in EST, setting a task for March 15 at 11 PM stores it as March 16, 4 AM UTC—then displays "2025-03-16" when read back via slice(0, 10).
Instead, construct the Date from local year/month/day components and format the input consistently in local time:
Suggested fix
onChange={(e) => {
const [year, month, day] = e.target.value.split('-').map(Number);
const d = new Date(year, month - 1, day, 23, 0, 0, 0);
updateForm("date", d.toISOString());
}}And update the value to use a local date formatter:
value={formValues.date.split('T')[0].split('-').length === 3
? new Date(formValues.date).toLocaleDateString('sv-SE')
: ''}Or create a helper similar to formatDateForInput() that works bidirectionally with local dates.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/blade/src/app/_components/issues/create-edit-dialog.tsx` around lines
468 - 478, The date picker is using new Date(e.target.value) (which parses
YYYY-MM-DD as UTC) and then setHours in local time, causing day shifts; change
the onChange for the Input with id `${baseId}-task-due-date` to parse
e.target.value by splitting into year, month, day and construct the Date via new
Date(year, month-1, day, 23, 0, 0, 0) before calling updateForm("date",
d.toISOString()); also replace the value expression that uses
formValues.date.slice(0,10) with a local-time formatter (or add a helper like
formatDateForInput(formValues.date)) that returns the YYYY-MM-DD string in local
time so the input and stored ISO string remain consistent.
Why
For the Calendar/Task Manager sprint.
What
Issue(s): #398
Dialog to create or edit a new issue.
Test Plan
Merge me twin.
Checklist
db:pushbefore mergingSummary by CodeRabbit