Skip to content

feat: implement one-on-one meeting notes with Lexical rich text editor - #21

Open
fvishw wants to merge 1 commit into
mainfrom
17-ai-powered-performance-evaluation-reports-quarterly
Open

fvishw wants to merge 1 commit into
mainfrom
17-ai-powered-performance-evaluation-reports-quarterly

Conversation

@fvishw

@fvishw fvishw commented Feb 27, 2026 •

Copy link
Copy Markdown
Owner

Add full meeting management feature allowing admins to schedule 1:1 meetings with employees, write rich-text notes using a Lexical editor, and manage meetings through a filterable table view with CRUD operations.

Summary by CodeRabbit

  • New Features

    • Added "1:1 Meetings" section to manage meeting records: schedule, edit, delete, and track status.
    • Integrated rich text editor for detailed meeting notes with formatting options.
    • Added multi-field filtering by employee, quarter, year, and meeting status.
    • Enhanced goal filtering with quarter and year options.
  • Improvements

    • Improved input validation for goals and meeting fields.

Add full meeting management feature allowing admins to schedule 1:1 meetings
with employees, write rich-text notes using a Lexical editor, and manage
meetings through a filterable table view with CRUD operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 27, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive one-on-one meetings feature across the client and server, including meeting CRUD operations, filtering capabilities, a rich text editor for notes, and supporting API, database, and UI infrastructure.

Changes

Cohort / File(s) Summary
Dependencies
client/package.json
Added Lexical editor library dependencies (@lexical/html, @lexical/link, @lexical/list, @lexical/react, @lexical/rich-text, and lexical core package) at version ^0.41.0 for rich text editing.
API Types & Integration
client/src/api/api.ts, client/src/types/apiResponse.ts
Extended API surface with meeting CRUD methods (create, update, delete, getAllMeetings, getMeetingById) and new response types (Meeting, GetAllMeetings, GetMeeting). Enhanced getGoalsByOwner with optional quarter/year filtering.
Meeting Components
client/src/components/meetings/LexicalEditor.tsx, client/src/components/meetings/MeetingFilter.tsx, client/src/components/meetings/MeetingFormDialog.tsx, client/src/components/meetings/MeetingNotesDialog.tsx, client/src/components/meetings/DeleteMeetingModal.tsx, client/src/components/meetings/meetingTable.config.tsx, client/src/components/meetings/meetingTableAction.tsx
Created new React components for meetings UI: rich text editor, multi-field filter, form dialog for scheduling/editing, notes dialog, delete confirmation modal, table configuration with status mapping, and row action menu with modal management.
Meeting Management & Layout
client/src/components/meetings/MeetingManagement.tsx, client/src/pages/outlets/meetingsLayout.tsx, client/src/routes/routes.ts
Added MeetingManagement component with filtering and React Query integration, page layout wrapper, and new protected route at /meetings.
Navigation & Goal Filtering
client/src/components/side-bar-items.ts, client/src/components/myGoals/myGoals.tsx
Added "1:1 Meetings" sidebar item; extended myGoals component with quarter/year filtering controls.
Server Model & Validation
server/src/models/meeting.model.ts, server/src/types/meeting.ts, server/src/types/goal.ts
Introduced Meeting Mongoose model with schema, indexes, and Zod validation schemas (MeetingSchema, UpdateMeetingSchema). Added non-empty string validation for goal and subtask titles.
Server Controller & Routes
server/src/controllers/meeting.controller.ts, server/src/routes/meeting.route.ts, server/src/app.ts, server/src/controllers/goal.controller.ts
Implemented full meeting CRUD controller with validation, filtering, and soft-delete. Registered meeting routes with admin auth middleware. Updated goal controller to support dynamic quarter/year filtering in queries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 A meetings editor, lexical and bright,
Rich notes and filters make conversations right.
From client to server, full CRUD takes flight,
One-on-ones now shine with schema and might!
✨ hops excitedly

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: implementing one-on-one meeting notes with Lexical rich text editor. The changeset includes comprehensive meeting management features (CRUD operations, filtering, table views) centered around the Lexical editor for meeting notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 17-ai-powered-performance-evaluation-reports-quarterly

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfaf1dbdfc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +102 to +106
if (quarter) {
filter.quarter = quarter;
}
if (year) {
filter.year = Number(year);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip ALL values when building meeting quarter/year filters

The UI filter allows users to pick ALL for quarter/year, but this handler applies those sentinel values directly (quarter: "ALL" and year: Number("ALL")), which turns the query into a non-matching filter and makes the table look empty instead of clearing the filter. In practice, choosing “All quarters” or “All years” breaks the main meeting list until filters are reset.

Useful? React with 👍 / 👎.

Comment on lines +70 to +74
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner />
</div>
) : (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Block note editing when initial meeting fetch fails

This render path only distinguishes loading vs non-loading, so if getMeetingById fails the dialog still shows an editable empty editor and keeps Save enabled. Because Save posts notesState (default empty string) and sets status to completed, a transient read failure can lead to overwriting existing notes without ever loading them. Add an explicit error state (or disable saving) when the fetch fails.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (4)
client/src/components/meetings/MeetingFormDialog.tsx (1)

80-90: Validation is duplicated between register and onSubmit.

The fields use register("title", { required: true }) and register("meetingDate", { required: true }), but the validation is also manually checked in onSubmit. Since form errors from react-hook-form are not displayed in the UI, consider either:

  1. Removing required: true from register and keeping the manual toast validation, or
  2. Using formState.errors to display inline error messages and removing the manual checks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/MeetingFormDialog.tsx` around lines 80 - 90,
The onSubmit handler duplicates validation already declared in register; drop
the manual checks in onSubmit and rely on react-hook-form validation: keep the
required rules in register("title", ...) and register("meetingDate", ...),
ensure the form is submitted via handleSubmit(onSubmit), and surface validation
feedback by reading formState.errors in MeetingFormDialog to render inline error
messages (or toasts) for each field instead of performing manual checks before
calling mutate(data).
server/src/routes/meeting.route.ts (1)

15-17: Inconsistent REST API design for delete endpoint.

The DELETE /delete route expects meetingId as a query parameter (per the client API: deleteMeeting calls /meetings/delete with query param), while PUT /update/:meetingId uses a path parameter. This inconsistency can confuse API consumers.

Consider using a path parameter for consistency:

♻️ Suggested refactor for consistent REST design
-router.delete("/delete", authMiddleware(["admin"]), deleteMeeting);
+router.delete("/delete/:meetingId", authMiddleware(["admin"]), deleteMeeting);

This would require updating the client API call and the controller to read from req.params.meetingId instead of req.query.meetingId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/routes/meeting.route.ts` around lines 15 - 17, The delete route is
inconsistent with the update route: router.put("/update/:meetingId",
authMiddleware(["admin"]), updateMeeting) uses a path param but
router.delete("/delete", authMiddleware(["admin"]), deleteMeeting) expects a
query param; change the DELETE route to use a path parameter (e.g.,
"/delete/:meetingId") and update the deleteMeeting controller to read the id
from req.params.meetingId (and update any client calls to pass the id in the
URL), keeping authMiddleware(["admin"]) unchanged to preserve authorization
behavior.
client/src/components/meetings/MeetingFilter.tsx (1)

35-47: Redundant loading state handling.

The early return on line 35-37 already handles the loading state by returning "Loading users...", but isLoading is still passed to UserSelect on line 46. Since the component returns early when loading, this prop will always be false when UserSelect renders.

Consider removing either the early return or the isLoading prop for consistency. The UserSelect component already shows a Spinner when isLoading is true (per the relevant code snippet), so removing the early return and relying solely on UserSelect's built-in loading state would provide a better UX by keeping the other filters visible.

♻️ Suggested refactor: Remove early return and rely on UserSelect's loading state
 const MeetingFilter = ({ filter, onChange }: MeetingFilterProps) => {
   const { data: users, isLoading } = useUser();
 
-  if (isLoading) {
-    return <div>Loading users...</div>;
-  }
-
   return (
     <div className="flex justify-end mb-4 gap-4">
       <UserSelect
         users={users || []}
         value={filter.employeeId || undefined}
         onChange={(val) => onChange({ ...filter, employeeId: val })}
         placeholder="Filter by employee"
         isLoading={isLoading}
         allowAllOption
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/MeetingFilter.tsx` around lines 35 - 47, The
component MeetingFilter currently returns early when isLoading, hiding other
filters; remove the early return block that renders "Loading users..." and
instead keep rendering the main JSX so UserSelect (prop isLoading) can display
its internal Spinner; update the component by deleting the if (isLoading) {
return <div>Loading users...</div>; } branch and ensure UserSelect continues to
receive isLoading, users, value={filter.employeeId || undefined}, onChange,
placeholder, and allowAllOption so the rest of the filter UI remains visible
while users load.
server/src/controllers/meeting.controller.ts (1)

112-115: Add pagination controls to the listing query.

The current query is unbounded. As meeting volume grows, this endpoint will get slower and heavier to render in the table. Consider limit/skip (or cursor pagination) plus a total count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/controllers/meeting.controller.ts` around lines 112 - 115, The
Meeting.find query is unbounded; modify the controller around
Meeting.find(...).populate(...).sort(...) to accept pagination params (e.g.,
page and limit or cursor) from the request, apply .skip((page-1)*limit) and
.limit(limit) (or implement cursor-based pagination) to the query, and also run
Meeting.countDocuments(filter) to return a total count alongside the paginated
meetings so the front end can render pages; ensure sensible defaults and
validate/parse req.query values before use and keep existing .populate(...) and
.sort({ meetingDate: -1 }) calls intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/meetings/LexicalEditor.tsx`:
- Around line 185-186: Replace the inline fragment passed to the RichTextPlugin
ErrorBoundary with LexicalErrorBoundary: import { LexicalErrorBoundary } from
'@lexical/react/LexicalErrorBoundary' in LexicalEditor.tsx and pass
ErrorBoundary={({ children, error }) => <LexicalErrorBoundary
error={error}>{children}</LexicalErrorBoundary>} (or simply
ErrorBoundary={LexicalErrorBoundary}) so decorator/render errors are caught by
the RichTextPlugin's ErrorBoundary instead of being forwarded by a bare
fragment.
- Around line 134-145: The effect in LexicalEditor.tsx currently guards with the
`loaded` flag so updates to `initialState` are ignored after the first load;
also it unnecessarily calls JSON.parse before `editor.parseEditorState` which
expects a string. Fix by removing the `loaded` gating (or update the condition
to respond when `initialState` changes), call
`editor.parseEditorState(initialState)` directly (no JSON.parse), then
`editor.setEditorState(...)` and update any state (remove or update `loaded`)
accordingly; keep the try/catch around parse/set to ignore invalid input or log
the error. Ensure the effect depends on `editor` and `initialState` so switching
meetings updates the editor content.

In `@client/src/components/meetings/MeetingFormDialog.tsx`:
- Around line 41-50: The form's defaultValues passed into useForm in
MeetingFormDialog are only applied on mount, so when editData changes the form
isn't updated; fix by adding a useEffect that watches editData and calls
reset(...) (or setValue for individual fields) to rehydrate the form with the
new values (ensure you convert editData.meetingDate to the same yyyy-mm-dd
string used previously). Use the existing reset and setValue helpers from
useForm and update the form whenever editData changes.

In `@client/src/components/meetings/MeetingNotesDialog.tsx`:
- Around line 70-87: The Save button in MeetingNotesDialog should be disabled
while data is loading or when the query errors; update the hook call that uses
useQuery to also destructure isError and error, then change the Save Button
disabled prop to disabled={isPending || isLoading || isError} (affecting the
Button inside DialogFooter and the handleSave flow). Also render a simple error
message when isError is true (near the editor area) using the error value to
show failure details so users know the fetch failed; keep
setNotesState/initialState handling the same.
- Around line 35-40: The select callback in useQuery (the select: (data) => {
... } block) must not perform side effects like calling setNotesState and should
not skip empty-string notes; remove the setNotesState call from select and
always return data.meeting. Instead, hydrate the notes state (including empty
string) from the query result in a side-effect handler such as the query's
onSuccess or a useEffect inside MeetingNotesDialog that watches the query
data/meeting and calls setNotesState(data.meeting.notes) unconditionally (no
falsy check) so switching dialogs won't show stale notes.

In `@client/src/components/meetings/meetingTable.config.tsx`:
- Around line 49-51: The formattedDate logic can output "Invalid Date" for
malformed meetingDate values; change the guard to validate the parsed date
before formatting by using dayjs(raw).isValid() (or equivalent) so that in the
meetingTable code path (row.getValue("meetingDate") → formattedDate) you return
"-" when the value is missing or dayjs(raw).isValid() is false, otherwise format
with dayjs(raw).format("D MMM YY").
- Around line 58-64: The code uses unsafe lookups/casts: when rendering Badge it
reads status via row.getValue("status") then looks up statusMapping[status] and
statusStyles[currentStatus] and force-casts to IBadgeVariant which can produce
undefined; update this by defining a typed status union/enum and a typed mapping
object, then resolve currentStatus safely (e.g., const currentStatus =
statusMapping[status] ?? "Unknown") and choose a fallback statusStyle (e.g.,
statusStyles["Unknown"] or a default variant) before passing it to <Badge> so
you never use `as` and always render a defined label and variant.

In `@client/src/components/meetings/meetingTableAction.tsx`:
- Around line 81-85: The editData object passed in MeetingTableAction includes
employee: "" which is unused in edit mode; remove the employee property from the
editData object (in the component that constructs editData for edit mode) or, if
you prefer to keep it, replace it with a clear inline comment explaining it's
intentionally omitted/immutable in edit mode; update the code that builds
editData in meetingTableAction (where editData is created for a given meeting)
and reference MeetingFormDialog and updateMeeting to ensure consistency with the
form and API (since updateMeeting excludes employee).

In `@server/src/controllers/goal.controller.ts`:
- Around line 258-260: Validate the incoming year query before coercion in the
goal controller: check that the year string (variable year in the controller)
matches an expected numeric format (e.g. /^\d+$/ or parseInt producing a finite
integer) and only then set filter.year = Number(year); if the value is invalid
(NaN or non-integer) either omit filter.year or return a 400 validation error
depending on existing API behavior; update the conditional around year !== "ALL"
in the controller to perform this validation before assigning to filter.year.

In `@server/src/controllers/meeting.controller.ts`:
- Around line 21-24: The code only validates the employee ID format
(Types.ObjectId.isValid) but must also verify the employee actually exists and
isn't deleted before creating a meeting: in the create handler (e.g., the
function in meeting.controller.ts where parsedPayload.data and employee are
used), replace/augment the shape check with a DB lookup using your Employee
model (e.g., Employee.findById(parsedPayload.data.employee)) and verify the
record exists and is active (check fields like deleted/isActive as applicable);
if not found or marked deleted, throw an ApiError (404 or 400 per project
convention) instead of proceeding to create the meeting.
- Around line 99-110: Validate and normalize incoming query params before
mutating filter: check employeeId with mongoose.Types.ObjectId.isValid (or
equivalent) and only set filter.employee when valid; parse year with
parseInt/Number and ensure Number.isInteger(year) and a sensible range before
setting filter.year; validate quarter against the allowed values/pattern (e.g.,
"Q1".."Q4" or 1..4) before setting filter.quarter; validate status against an
allowedStatus array and only set filter.status when it matches (or return a 400
BadRequest for explicit invalid input). Update the logic around the variables
employeeId, quarter, year, status in meeting.controller.ts to perform these
checks/normalizations and either skip invalid values or respond with a
validation error instead of inserting raw query values into filter.

In `@server/src/types/meeting.ts`:
- Around line 7-8: The create validation leaves status undefined; update the Zod
status field in server/src/types/meeting.ts so new meetings get a default (e.g.,
"scheduled"). Replace the current status: z.enum(["scheduled", "completed",
"cancelled"]).optional() with a defaulted enum (either
z.enum([...]).default("scheduled") or
z.enum([...]).optional().default("scheduled")) so the API/UI always receives a
concrete status value.

---

Nitpick comments:
In `@client/src/components/meetings/MeetingFilter.tsx`:
- Around line 35-47: The component MeetingFilter currently returns early when
isLoading, hiding other filters; remove the early return block that renders
"Loading users..." and instead keep rendering the main JSX so UserSelect (prop
isLoading) can display its internal Spinner; update the component by deleting
the if (isLoading) { return <div>Loading users...</div>; } branch and ensure
UserSelect continues to receive isLoading, users, value={filter.employeeId ||
undefined}, onChange, placeholder, and allowAllOption so the rest of the filter
UI remains visible while users load.

In `@client/src/components/meetings/MeetingFormDialog.tsx`:
- Around line 80-90: The onSubmit handler duplicates validation already declared
in register; drop the manual checks in onSubmit and rely on react-hook-form
validation: keep the required rules in register("title", ...) and
register("meetingDate", ...), ensure the form is submitted via
handleSubmit(onSubmit), and surface validation feedback by reading
formState.errors in MeetingFormDialog to render inline error messages (or
toasts) for each field instead of performing manual checks before calling
mutate(data).

In `@server/src/controllers/meeting.controller.ts`:
- Around line 112-115: The Meeting.find query is unbounded; modify the
controller around Meeting.find(...).populate(...).sort(...) to accept pagination
params (e.g., page and limit or cursor) from the request, apply
.skip((page-1)*limit) and .limit(limit) (or implement cursor-based pagination)
to the query, and also run Meeting.countDocuments(filter) to return a total
count alongside the paginated meetings so the front end can render pages; ensure
sensible defaults and validate/parse req.query values before use and keep
existing .populate(...) and .sort({ meetingDate: -1 }) calls intact.

In `@server/src/routes/meeting.route.ts`:
- Around line 15-17: The delete route is inconsistent with the update route:
router.put("/update/:meetingId", authMiddleware(["admin"]), updateMeeting) uses
a path param but router.delete("/delete", authMiddleware(["admin"]),
deleteMeeting) expects a query param; change the DELETE route to use a path
parameter (e.g., "/delete/:meetingId") and update the deleteMeeting controller
to read the id from req.params.meetingId (and update any client calls to pass
the id in the URL), keeping authMiddleware(["admin"]) unchanged to preserve
authorization behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c765e42 and dfaf1db.

⛔ Files ignored due to path filters (1)
  • client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • client/package.json
  • client/src/api/api.ts
  • client/src/components/meetings/DeleteMeetingModal.tsx
  • client/src/components/meetings/LexicalEditor.tsx
  • client/src/components/meetings/MeetingFilter.tsx
  • client/src/components/meetings/MeetingFormDialog.tsx
  • client/src/components/meetings/MeetingManagement.tsx
  • client/src/components/meetings/MeetingNotesDialog.tsx
  • client/src/components/meetings/meetingTable.config.tsx
  • client/src/components/meetings/meetingTableAction.tsx
  • client/src/components/myGoals/myGoals.tsx
  • client/src/components/side-bar-items.ts
  • client/src/pages/outlets/meetingsLayout.tsx
  • client/src/routes/routes.ts
  • client/src/types/apiResponse.ts
  • server/src/app.ts
  • server/src/controllers/goal.controller.ts
  • server/src/controllers/meeting.controller.ts
  • server/src/models/meeting.model.ts
  • server/src/routes/meeting.route.ts
  • server/src/types/goal.ts
  • server/src/types/meeting.ts

Comment on lines +134 to +145
useEffect(() => {
if (initialState && !loaded) {
try {
const parsed = JSON.parse(initialState);
const editorState = editor.parseEditorState(parsed);
editor.setEditorState(editorState);
setLoaded(true);
} catch {
// Invalid state, ignore
}
}
}, [editor, initialState, loaded]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "LexicalEditor.tsx" | head -20

Repository: fvishw/PMS

Length of output: 104


🏁 Script executed:

cat -n ./client/src/components/meetings/LexicalEditor.tsx | head -200

Repository: fvishw/PMS

Length of output: 6649


🏁 Script executed:

rg -t typescript -t tsx "LexicalEditor" --type-add 'tsx:*.tsx' -B 2 -A 2 | grep -v "^node_modules" | head -100

Repository: fvishw/PMS

Length of output: 88


🏁 Script executed:

rg "LexicalEditor" -B 2 -A 2 --glob "*.tsx" --glob "*.ts" | head -150

Repository: fvishw/PMS

Length of output: 2546


🏁 Script executed:

cat -n ./client/src/components/meetings/MeetingNotesDialog.tsx

Repository: fvishw/PMS

Length of output: 3285


🏁 Script executed:

rg "parseEditorState" -B 2 -A 2 --glob "*.ts" --glob "*.tsx" | head -80

Repository: fvishw/PMS

Length of output: 489


🏁 Script executed:

rg "parseEditorState|useLexicalComposerContext" -B 5 -A 5 ./client/src/components/meetings/LexicalEditor.tsx | head -60

Repository: fvishw/PMS

Length of output: 1439


🏁 Script executed:

cat ./client/package.json | grep -A 5 -B 5 "lexical"

Repository: fvishw/PMS

Length of output: 930


🏁 Script executed:

web_search
Lexical parseEditorState API documentation lexical 0.41.0

Repository: fvishw/PMS

Length of output: 150


🌐 Web query:

Lexical editor parseEditorState API documentation

💡 Result:

parseEditorState is a LexicalEditor API used to deserialize a serialized editor state back into an EditorState, typically after you previously serialized with editor.getEditorState().toJSON() and JSON.stringify(...). (npmjs.com)

Signature (TypeScript) (as documented in generated Lexical API docs):

  • editor.parseEditorState(maybeStringifiedEditorState: string): EditorState (tessl.io)

Typical usage

const serialized = JSON.stringify(editor.getEditorState().toJSON());
const nextState = editor.parseEditorState(serialized);
editor.setEditorState(nextState);

(npmjs.com)

Related docs/context

  • Lexical editor states are designed to be JSON-serializable (toJSON()) and restorable via editor.parseEditorState(...). (deepwiki.com)

Citations:


Stale content when initialState prop changes because loaded flag only checks once.

The loaded boolean prevents the effect from re-running if initialState changes after the initial load. If the same editor instance is reused for different meeting notes (e.g., switching meetings without closing the dialog), the editor will display the old notes.

Additionally, JSON.parse is unnecessary since parseEditorState expects a stringified JSON state, not a parsed object.

Suggested fix
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
@@
 function LoadInitialStatePlugin({
   initialState,
 }: {
   initialState?: string;
 }) {
   const [editor] = useLexicalComposerContext();
-  const [loaded, setLoaded] = useState(false);
+  const lastLoadedStateRef = useRef<string | null>(null);
 
   useEffect(() => {
-    if (initialState && !loaded) {
+    if (
+      typeof initialState === "string" &&
+      initialState !== lastLoadedStateRef.current
+    ) {
       try {
-        const parsed = JSON.parse(initialState);
-        const editorState = editor.parseEditorState(parsed);
+        const editorState = editor.parseEditorState(initialState);
         editor.setEditorState(editorState);
-        setLoaded(true);
+        lastLoadedStateRef.current = initialState;
       } catch {
         // Invalid state, ignore
       }
     }
-  }, [editor, initialState, loaded]);
+  }, [editor, initialState]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/LexicalEditor.tsx` around lines 134 - 145, The
effect in LexicalEditor.tsx currently guards with the `loaded` flag so updates
to `initialState` are ignored after the first load; also it unnecessarily calls
JSON.parse before `editor.parseEditorState` which expects a string. Fix by
removing the `loaded` gating (or update the condition to respond when
`initialState` changes), call `editor.parseEditorState(initialState)` directly
(no JSON.parse), then `editor.setEditorState(...)` and update any state (remove
or update `loaded`) accordingly; keep the try/catch around parse/set to ignore
invalid input or log the error. Ensure the effect depends on `editor` and
`initialState` so switching meetings updates the editor content.

Comment on lines +185 to +186
ErrorBoundary={({ children }) => <>{children}</>}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "LexicalEditor.tsx" --type f

Repository: fvishw/PMS

Length of output: 102


🏁 Script executed:

sed -n '175,195p' client/src/components/meetings/LexicalEditor.tsx | cat -n

Repository: fvishw/PMS

Length of output: 824


🏁 Script executed:

head -50 client/src/components/meetings/LexicalEditor.tsx | cat -n

Repository: fvishw/PMS

Length of output: 1935


🏁 Script executed:

rg "LexicalErrorBoundary" --type tsx --type ts -A 2 -B 2

Repository: fvishw/PMS

Length of output: 81


🏁 Script executed:

rg "RichTextPlugin" --type tsx --type ts -A 3 -B 1

Repository: fvishw/PMS

Length of output: 81


🏁 Script executed:

rg "from [\"']@lexical" --type tsx --type ts | head -20

Repository: fvishw/PMS

Length of output: 81


🌐 Web query:

Lexical React LexicalErrorBoundary RichTextPlugin error handling

💡 Result:

In @lexical/react, RichTextPlugin requires an ErrorBoundary component (prop name is ErrorBoundary) that Lexical uses to wrap React-rendered DecoratorNodes so a crashing decorator doesn’t take down the whole editor. This is why you commonly pass the built-in LexicalErrorBoundary. [1], [2]

Recommended setup

import {LexicalComposer} from "@lexical/react/LexicalComposer";
import {RichTextPlugin} from "@lexical/react/LexicalRichTextPlugin";
import {ContentEditable} from "@lexical/react/LexicalContentEditable";
import {LexicalErrorBoundary} from "@lexical/react/LexicalErrorBoundary";

const initialConfig = {
  namespace: "MyEditor",
  onError: (error: unknown) => {
    console.error(error);
    throw error; // optional: surface the error to your app/runtime
  },
};

export function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={null}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}

This combines:

  • Editor-level error handling via initialConfig.onError (log/report, optionally rethrow). [3]
  • Decorator render error isolation via RichTextPlugin’s ErrorBoundary={LexicalErrorBoundary}. [1], [2]

If you need a custom boundary

Your ErrorBoundary must match Lexical’s expected shape: it receives children plus an onError(error) callback (which Lexical will provide). LexicalErrorBoundary is exactly that. [4]

Sources:
[1] RichTextPlugin ErrorBoundary prop type definition (UNPKG) (app.unpkg.com)
[2] Lexical release note explaining RichTextPlugin now requires an ErrorBoundary (newreleases.io)
[3] Example LexicalComposer initialConfig.onError pattern used with RichTextPlugin (liveblocks.io)
[4] LexicalErrorBoundary props (children, onError) type (UNPKG) (app.unpkg.com)

Citations:


Use LexicalErrorBoundary from @lexical/react/LexicalErrorBoundary for the ErrorBoundary prop.

A fragment wrapper passes through children without catching errors. RichTextPlugin's ErrorBoundary prop is specifically designed to contain decorator render errors and prevent them from crashing the entire editor. Replace the inline fragment with the proper Lexical error boundary.

Suggested fix
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
@@
-            ErrorBoundary={({ children }) => <>{children}</>}
+            ErrorBoundary={LexicalErrorBoundary}
📝 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.

Suggested change
ErrorBoundary={({ children }) => <>{children}</>}
/>
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
// ... other code ...
ErrorBoundary={LexicalErrorBoundary}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/LexicalEditor.tsx` around lines 185 - 186,
Replace the inline fragment passed to the RichTextPlugin ErrorBoundary with
LexicalErrorBoundary: import { LexicalErrorBoundary } from
'@lexical/react/LexicalErrorBoundary' in LexicalEditor.tsx and pass
ErrorBoundary={({ children, error }) => <LexicalErrorBoundary
error={error}>{children}</LexicalErrorBoundary>} (or simply
ErrorBoundary={LexicalErrorBoundary}) so decorator/render errors are caught by
the RichTextPlugin's ErrorBoundary instead of being forwarded by a bare
fragment.

Comment on lines +41 to +50
const { reset, handleSubmit, register, setValue, watch } =
useForm<MeetingFormValues>({
defaultValues: {
title: editData?.title || "",
employee: editData?.employee || "",
meetingDate: editData?.meetingDate
? new Date(editData.meetingDate).toISOString().split("T")[0]
: "",
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Form does not reset when editData changes.

The defaultValues in useForm are only evaluated once during initial render. If the dialog is opened with different editData (e.g., editing a different meeting), the form will display stale values from the previous edit session.

🐛 Proposed fix: Reset form when editData changes
+import { useEffect } from "react";
+
 function MeetingFormDialog({ isOpen, onClose, editData }: MeetingFormDialogProps) {
   const { data: users, isLoading: usersLoading } = useUser();
   const isEditMode = !!editData;
 
   const { reset, handleSubmit, register, setValue, watch } =
     useForm<MeetingFormValues>({
       defaultValues: {
         title: editData?.title || "",
         employee: editData?.employee || "",
         meetingDate: editData?.meetingDate
           ? new Date(editData.meetingDate).toISOString().split("T")[0]
           : "",
       },
     });
 
+  useEffect(() => {
+    if (isOpen) {
+      reset({
+        title: editData?.title || "",
+        employee: editData?.employee || "",
+        meetingDate: editData?.meetingDate
+          ? new Date(editData.meetingDate).toISOString().split("T")[0]
+          : "",
+      });
+    }
+  }, [isOpen, editData, reset]);
+
   const employeeValue = watch("employee");
📝 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.

Suggested change
const { reset, handleSubmit, register, setValue, watch } =
useForm<MeetingFormValues>({
defaultValues: {
title: editData?.title || "",
employee: editData?.employee || "",
meetingDate: editData?.meetingDate
? new Date(editData.meetingDate).toISOString().split("T")[0]
: "",
},
});
import { useEffect } from "react";
function MeetingFormDialog({ isOpen, onClose, editData }: MeetingFormDialogProps) {
const { data: users, isLoading: usersLoading } = useUser();
const isEditMode = !!editData;
const { reset, handleSubmit, register, setValue, watch } =
useForm<MeetingFormValues>({
defaultValues: {
title: editData?.title || "",
employee: editData?.employee || "",
meetingDate: editData?.meetingDate
? new Date(editData.meetingDate).toISOString().split("T")[0]
: "",
},
});
useEffect(() => {
if (isOpen) {
reset({
title: editData?.title || "",
employee: editData?.employee || "",
meetingDate: editData?.meetingDate
? new Date(editData.meetingDate).toISOString().split("T")[0]
: "",
});
}
}, [isOpen, editData, reset]);
const employeeValue = watch("employee");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/MeetingFormDialog.tsx` around lines 41 - 50,
The form's defaultValues passed into useForm in MeetingFormDialog are only
applied on mount, so when editData changes the form isn't updated; fix by adding
a useEffect that watches editData and calls reset(...) (or setValue for
individual fields) to rehydrate the form with the new values (ensure you convert
editData.meetingDate to the same yyyy-mm-dd string used previously). Use the
existing reset and setValue helpers from useForm and update the form whenever
editData changes.

Comment on lines +35 to +40
select: (data) => {
if (data.meeting.notes) {
setNotesState(data.meeting.notes);
}
return data.meeting;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "MeetingNotesDialog.tsx" --type f

Repository: fvishw/PMS

Length of output: 107


🏁 Script executed:

cat -n client/src/components/meetings/MeetingNotesDialog.tsx

Repository: fvishw/PMS

Length of output: 3285


🏁 Script executed:

rg "notesState" client/src/components/meetings/MeetingNotesDialog.tsx

Repository: fvishw/PMS

Length of output: 188


🏁 Script executed:

rg "MeetingNotesDialog" client/src --type tsx --type ts -A 3 -B 1

Repository: fvishw/PMS

Length of output: 81


🏁 Script executed:

rg "MeetingNotesDialog" client/src -A 3 -B 1

Repository: fvishw/PMS

Length of output: 2198


🏁 Script executed:

rg "LexicalEditor" client/src/components/meetings/MeetingNotesDialog.tsx -A 5 -B 2

Repository: fvishw/PMS

Length of output: 467


Do not set component state inside useQuery.select, and always hydrate empty notes.

The select callback will skip empty-string notes (falsy check), leaving stale notes from a previous meeting visible when switching between dialogs. This also violates React Query best practices by putting side effects in a selector.

✅ Suggested fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
-  const { isLoading } = useQuery({
+  const { data: meeting, isLoading } = useQuery({
     queryKey: ["meeting", meetingId],
     queryFn: () => Api.getMeetingById(meetingId),
     enabled: !!meetingId,
-    select: (data) => {
-      if (data.meeting.notes) {
-        setNotesState(data.meeting.notes);
-      }
-      return data.meeting;
-    },
+    select: (data) => data.meeting,
   });
+
+  useEffect(() => {
+    setNotesState(meeting?.notes ?? "");
+  }, [meetingId, meeting?.notes]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/MeetingNotesDialog.tsx` around lines 35 - 40,
The select callback in useQuery (the select: (data) => { ... } block) must not
perform side effects like calling setNotesState and should not skip empty-string
notes; remove the setNotesState call from select and always return data.meeting.
Instead, hydrate the notes state (including empty string) from the query result
in a side-effect handler such as the query's onSuccess or a useEffect inside
MeetingNotesDialog that watches the query data/meeting and calls
setNotesState(data.meeting.notes) unconditionally (no falsy check) so switching
dialogs won't show stale notes.

Comment on lines +70 to +87
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Spinner />
</div>
) : (
<LexicalEditor
onChange={setNotesState}
initialState={notesState || undefined}
/>
)}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isPending}>
{isPending ? "Saving..." : "Save Notes"}
</Button>
</DialogFooter>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "MeetingNotesDialog.tsx" -type f

Repository: fvishw/PMS

Length of output: 109


🏁 Script executed:

wc -l ./client/src/components/meetings/MeetingNotesDialog.tsx

Repository: fvishw/PMS

Length of output: 112


🏁 Script executed:

cat -n ./client/src/components/meetings/MeetingNotesDialog.tsx

Repository: fvishw/PMS

Length of output: 3285


Disable save button while data is loading and add error handling.

The Save Notes button is currently clickable while isLoading is true because it only checks isPending. Although the editor is hidden during load, the button state should reflect the actual data readiness. Additionally, the query lacks error state handling.

Extract isError and error from the useQuery hook, disable the button with disabled={isPending || isLoading || isError}, and display an error message when the fetch fails:

Suggested fix
-  const { isLoading } = useQuery({
+  const { isLoading, isError, error } = useQuery({
     queryKey: ["meeting", meetingId],
     queryFn: () => Api.getMeetingById(meetingId),
     enabled: !!meetingId,
     select: (data) => {
       if (data.meeting.notes) {
         setNotesState(data.meeting.notes);
       }
       return data.meeting;
     },
   });

         {isLoading ? (
           <div className="flex items-center justify-center py-8">
             <Spinner />
           </div>
+        ) : isError ? (
+          <p className="py-4 text-sm text-destructive">
+            {(error as Error)?.message || "Failed to load meeting notes."}
+          </p>
         ) : (
           <LexicalEditor
             onChange={setNotesState}
             initialState={notesState || undefined}
           />
         )}
         <DialogFooter>
           <Button variant="outline" onClick={onClose}>
             Cancel
           </Button>
-          <Button onClick={handleSave} disabled={isPending}>
+          <Button onClick={handleSave} disabled={isPending || isLoading || isError}>
             {isPending ? "Saving..." : "Save Notes"}
           </Button>
         </DialogFooter>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/MeetingNotesDialog.tsx` around lines 70 - 87,
The Save button in MeetingNotesDialog should be disabled while data is loading
or when the query errors; update the hook call that uses useQuery to also
destructure isError and error, then change the Save Button disabled prop to
disabled={isPending || isLoading || isError} (affecting the Button inside
DialogFooter and the handleSave flow). Also render a simple error message when
isError is true (near the editor area) using the error value to show failure
details so users know the fetch failed; keep setNotesState/initialState handling
the same.

Comment on lines +81 to +85
editData={{
_id: meeting._id,
title: meeting.title,
employee: "",
meetingDate: meeting.meetingDate,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding MeetingFormDialog component ==="
fd 'MeetingFormDialog' -t f

echo
echo "=== Searching for MeetingFormDialog in the codebase ==="
rg -l 'MeetingFormDialog' --type=tsx --type=ts

echo
echo "=== Inspecting editData usage in meetingTableAction.tsx ==="
rg -n -B5 -A10 'editData' client/src/components/meetings/meetingTableAction.tsx

echo
echo "=== Checking MeetingFormDialog prop types and employee handling ==="
fd '.*' client/src/components/meetings -type f -name '*Dialog*' -o -name '*Form*'

Repository: fvishw/PMS

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading MeetingFormDialog.tsx ==="
wc -l client/src/components/meetings/MeetingFormDialog.tsx

echo
echo "=== Searching for editData and employee in MeetingFormDialog.tsx ==="
rg -n 'editData|employee|setFormData|setValue|initialValues' client/src/components/meetings/MeetingFormDialog.tsx

echo
echo "=== Reading meetingTableAction.tsx around lines 81-85 ==="
sed -n '70,100p' client/src/components/meetings/meetingTableAction.tsx

echo
echo "=== Checking how editData is passed to MeetingFormDialog ==="
rg -n -B3 -A3 'MeetingFormDialog' client/src/components/meetings/meetingTableAction.tsx

Repository: fvishw/PMS

Length of output: 2521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking MeetingRow type definition for employee field ==="
rg -n -B5 -A15 'type MeetingRow\|interface MeetingRow' client/src/components/meetings/

echo
echo "=== Checking if meeting object has employee property in meetingTableAction ==="
sed -n '1,40p' client/src/components/meetings/meetingTableAction.tsx

echo
echo "=== Full context of meetingTableAction to understand meeting object structure ==="
rg -n 'meeting\.' client/src/components/meetings/meetingTableAction.tsx | head -20

Repository: fvishw/PMS

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding meetingTable.config file ==="
fd 'meetingTable.config' -t f

echo
echo "=== Reading meetingTable.config file ==="
cat client/src/components/meetings/meetingTable.config.ts

echo
echo "=== Full context of meetingTableAction.tsx to see meeting usage ==="
head -50 client/src/components/meetings/meetingTableAction.tsx

Repository: fvishw/PMS

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading meetingTable.config.tsx ==="
cat client/src/components/meetings/meetingTable.config.tsx

echo
echo "=== Reading full meetingTableAction.tsx to understand meeting object ==="
head -120 client/src/components/meetings/meetingTableAction.tsx

Repository: fvishw/PMS

Length of output: 5425


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking API response type for meetings ==="
rg -n 'type.*Meeting|interface.*Meeting' client/src/api/ --type=ts

echo
echo "=== Checking if there's a fuller Meeting type (not just MeetingRow) ==="
fd 'Meeting' client/src --type=ts --type=tsx | head -10

echo
echo "=== Searching for where meetings are fetched/used ==="
rg -n 'Api.getMeetings\|Api.updateMeeting' client/src/ -A5 | head -40

Repository: fvishw/PMS

Length of output: 101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding api files ==="
fd 'api' client/src --type=ts --type=tsx | grep -i api

echo
echo "=== Reading api.ts or similar ==="
find client/src -name '*api*' -type f

echo
echo "=== Searching for updateMeeting implementation ==="
rg -n 'updateMeeting' client/src -A10 | head -50

echo
echo "=== Checking what the full meeting object structure is ==="
rg -n 'getMeetings\|fetchMeetings\|meetings' client/src/api -B2 -A5

Repository: fvishw/PMS

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding all api files ==="
find client/src -name '*api*' -type f 2>/dev/null

echo
echo "=== Searching for updateMeeting definition ==="
rg -n 'updateMeeting' client/src -A10 | head -50

echo
echo "=== Checking how meetings are displayed to understand data flow ==="
rg -n 'MeetingRow\|meeting\._id\|meeting\.employee' client/src/components/meetings/meetingTableAction.tsx

Repository: fvishw/PMS

Length of output: 2846


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading full MeetingFormDialog.tsx to check employee field handling ==="
cat client/src/components/meetings/MeetingFormDialog.tsx

echo
echo "=== Confirming what updateMeeting sends vs createMeeting ==="
rg -n 'createMeeting\|updateMeeting' client/src/components/meetings/MeetingFormDialog.tsx -B2 -A8

Repository: fvishw/PMS

Length of output: 4673


Remove or clarify the unused employee property in edit mode.

The employee field is conditionally rendered only in create mode (MeetingFormDialog line 120) and is not sent to the API during edit (updateMeeting excludes it). Passing employee: "" in the editData is unnecessary and creates confusion about form capabilities. Either omit the property entirely when editing, or add a comment explaining why it's empty:

editData={{
  _id: meeting._id,
  title: meeting.title,
  // employee field intentionally omitted in edit mode; cannot reassign meeting owner
  meetingDate: meeting.meetingDate,
}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/meetings/meetingTableAction.tsx` around lines 81 - 85,
The editData object passed in MeetingTableAction includes employee: "" which is
unused in edit mode; remove the employee property from the editData object (in
the component that constructs editData for edit mode) or, if you prefer to keep
it, replace it with a clear inline comment explaining it's intentionally
omitted/immutable in edit mode; update the code that builds editData in
meetingTableAction (where editData is created for a given meeting) and reference
MeetingFormDialog and updateMeeting to ensure consistency with the form and API
(since updateMeeting excludes employee).

Comment on lines +258 to +260
if (year && year !== "ALL") {
filter.year = Number(year);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate year before coercing to number.

Number(year) can become NaN for invalid query values, which silently builds a non-matching filter and returns confusing empty results.

✅ Suggested fix
-  if (year && year !== "ALL") {
-    filter.year = Number(year);
-  }
+  if (typeof year === "string" && year !== "ALL") {
+    const parsedYear = Number(year);
+    if (!Number.isInteger(parsedYear)) {
+      throw new ApiError(400, "Invalid year filter.");
+    }
+    filter.year = parsedYear;
+  }
📝 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.

Suggested change
if (year && year !== "ALL") {
filter.year = Number(year);
}
if (typeof year === "string" && year !== "ALL") {
const parsedYear = Number(year);
if (!Number.isInteger(parsedYear)) {
throw new ApiError(400, "Invalid year filter.");
}
filter.year = parsedYear;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/controllers/goal.controller.ts` around lines 258 - 260, Validate
the incoming year query before coercion in the goal controller: check that the
year string (variable year in the controller) matches an expected numeric format
(e.g. /^\d+$/ or parseInt producing a finite integer) and only then set
filter.year = Number(year); if the value is invalid (NaN or non-integer) either
omit filter.year or return a 400 validation error depending on existing API
behavior; update the conditional around year !== "ALL" in the controller to
perform this validation before assigning to filter.year.

Comment on lines +21 to +24
const { employee } = parsedPayload.data;
if (!Types.ObjectId.isValid(employee)) {
throw new ApiError(400, "Invalid employee ID");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate employee existence, not just ObjectId format.

Lines 21-24 only check ID shape. This still allows creating meetings for non-existent/deleted employees, producing orphan records.

Suggested fix
   const { employee } = parsedPayload.data;
   if (!Types.ObjectId.isValid(employee)) {
     throw new ApiError(400, "Invalid employee ID");
   }
+  const employeeExists = await Employee.exists({ _id: employee, isDeleted: false });
+  if (!employeeExists) {
+    throw new ApiError(404, "Employee not found");
+  }
📝 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.

Suggested change
const { employee } = parsedPayload.data;
if (!Types.ObjectId.isValid(employee)) {
throw new ApiError(400, "Invalid employee ID");
}
const { employee } = parsedPayload.data;
if (!Types.ObjectId.isValid(employee)) {
throw new ApiError(400, "Invalid employee ID");
}
const employeeExists = await Employee.exists({ _id: employee, isDeleted: false });
if (!employeeExists) {
throw new ApiError(404, "Employee not found");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/controllers/meeting.controller.ts` around lines 21 - 24, The code
only validates the employee ID format (Types.ObjectId.isValid) but must also
verify the employee actually exists and isn't deleted before creating a meeting:
in the create handler (e.g., the function in meeting.controller.ts where
parsedPayload.data and employee are used), replace/augment the shape check with
a DB lookup using your Employee model (e.g.,
Employee.findById(parsedPayload.data.employee)) and verify the record exists and
is active (check fields like deleted/isActive as applicable); if not found or
marked deleted, throw an ApiError (404 or 400 per project convention) instead of
proceeding to create the meeting.

Comment on lines +99 to +110
if (employeeId && employeeId !== "ALL") {
filter.employee = employeeId;
}
if (quarter) {
filter.quarter = quarter;
}
if (year) {
filter.year = Number(year);
}
if (status && status !== "ALL") {
filter.status = status;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate and normalize query filters before building Mongo criteria.

Lines 99-110 accept raw query params directly. Invalid values (e.g., malformed employeeId, year=abc, unsupported status) can lead to cast errors or incorrect query behavior.

Suggested fix
   if (employeeId && employeeId !== "ALL") {
+    if (!Types.ObjectId.isValid(String(employeeId))) {
+      throw new ApiError(400, "Invalid employee ID");
+    }
     filter.employee = employeeId;
   }
   if (quarter) {
-    filter.quarter = quarter;
+    const q = Number(quarter);
+    if (!Number.isInteger(q) || q < 1 || q > 4) {
+      throw new ApiError(400, "Invalid quarter");
+    }
+    filter.quarter = q;
   }
   if (year) {
-    filter.year = Number(year);
+    const y = Number(year);
+    if (!Number.isInteger(y) || y < 2000) {
+      throw new ApiError(400, "Invalid year");
+    }
+    filter.year = y;
   }
   if (status && status !== "ALL") {
+    if (!["scheduled", "completed", "cancelled"].includes(String(status))) {
+      throw new ApiError(400, "Invalid status");
+    }
     filter.status = status;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/controllers/meeting.controller.ts` around lines 99 - 110, Validate
and normalize incoming query params before mutating filter: check employeeId
with mongoose.Types.ObjectId.isValid (or equivalent) and only set
filter.employee when valid; parse year with parseInt/Number and ensure
Number.isInteger(year) and a sensible range before setting filter.year; validate
quarter against the allowed values/pattern (e.g., "Q1".."Q4" or 1..4) before
setting filter.quarter; validate status against an allowedStatus array and only
set filter.status when it matches (or return a 400 BadRequest for explicit
invalid input). Update the logic around the variables employeeId, quarter, year,
status in meeting.controller.ts to perform these checks/normalizations and
either skip invalid values or respond with a validation error instead of
inserting raw query values into filter.

Comment on lines +7 to +8
notes: z.string().optional().default(""),
status: z.enum(["scheduled", "completed", "cancelled"]).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Set a default status during create validation to keep API/UI contract consistent.

status is optional here, but the client table expects a required status union. Without a default, newly created meetings can carry undefined status values.

Suggested fix
-  status: z.enum(["scheduled", "completed", "cancelled"]).optional(),
+  status: z.enum(["scheduled", "completed", "cancelled"]).default("scheduled"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/types/meeting.ts` around lines 7 - 8, The create validation leaves
status undefined; update the Zod status field in server/src/types/meeting.ts so
new meetings get a default (e.g., "scheduled"). Replace the current status:
z.enum(["scheduled", "completed", "cancelled"]).optional() with a defaulted enum
(either z.enum([...]).default("scheduled") or
z.enum([...]).optional().default("scheduled")) so the API/UI always receives a
concrete status value.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant