Conversation
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>
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
💡 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".
| if (quarter) { | ||
| filter.quarter = quarter; | ||
| } | ||
| if (year) { | ||
| filter.year = Number(year); |
There was a problem hiding this comment.
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 👍 / 👎.
| {isLoading ? ( | ||
| <div className="flex items-center justify-center py-8"> | ||
| <Spinner /> | ||
| </div> | ||
| ) : ( |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (4)
client/src/components/meetings/MeetingFormDialog.tsx (1)
80-90: Validation is duplicated betweenregisterandonSubmit.The fields use
register("title", { required: true })andregister("meetingDate", { required: true }), but the validation is also manually checked inonSubmit. Since form errors from react-hook-form are not displayed in the UI, consider either:
- Removing
required: truefromregisterand keeping the manual toast validation, or- Using
formState.errorsto 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 /deleteroute expectsmeetingIdas a query parameter (per the client API:deleteMeetingcalls/meetings/deletewith query param), whilePUT /update/:meetingIduses 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.meetingIdinstead ofreq.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
isLoadingis still passed toUserSelecton line 46. Since the component returns early when loading, this prop will always befalsewhenUserSelectrenders.Consider removing either the early return or the
isLoadingprop for consistency. TheUserSelectcomponent already shows aSpinnerwhenisLoadingis true (per the relevant code snippet), so removing the early return and relying solely onUserSelect'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
⛔ Files ignored due to path filters (1)
client/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (22)
client/package.jsonclient/src/api/api.tsclient/src/components/meetings/DeleteMeetingModal.tsxclient/src/components/meetings/LexicalEditor.tsxclient/src/components/meetings/MeetingFilter.tsxclient/src/components/meetings/MeetingFormDialog.tsxclient/src/components/meetings/MeetingManagement.tsxclient/src/components/meetings/MeetingNotesDialog.tsxclient/src/components/meetings/meetingTable.config.tsxclient/src/components/meetings/meetingTableAction.tsxclient/src/components/myGoals/myGoals.tsxclient/src/components/side-bar-items.tsclient/src/pages/outlets/meetingsLayout.tsxclient/src/routes/routes.tsclient/src/types/apiResponse.tsserver/src/app.tsserver/src/controllers/goal.controller.tsserver/src/controllers/meeting.controller.tsserver/src/models/meeting.model.tsserver/src/routes/meeting.route.tsserver/src/types/goal.tsserver/src/types/meeting.ts
| 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]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "LexicalEditor.tsx" | head -20Repository: fvishw/PMS
Length of output: 104
🏁 Script executed:
cat -n ./client/src/components/meetings/LexicalEditor.tsx | head -200Repository: 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 -100Repository: fvishw/PMS
Length of output: 88
🏁 Script executed:
rg "LexicalEditor" -B 2 -A 2 --glob "*.tsx" --glob "*.ts" | head -150Repository: fvishw/PMS
Length of output: 2546
🏁 Script executed:
cat -n ./client/src/components/meetings/MeetingNotesDialog.tsxRepository: fvishw/PMS
Length of output: 3285
🏁 Script executed:
rg "parseEditorState" -B 2 -A 2 --glob "*.ts" --glob "*.tsx" | head -80Repository: fvishw/PMS
Length of output: 489
🏁 Script executed:
rg "parseEditorState|useLexicalComposerContext" -B 5 -A 5 ./client/src/components/meetings/LexicalEditor.tsx | head -60Repository: 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.0Repository: 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);Related docs/context
- Lexical editor states are designed to be JSON-serializable (
toJSON()) and restorable viaeditor.parseEditorState(...). (deepwiki.com)
Citations:
- 1: https://www.npmjs.com/package/lexical?utm_source=openai
- 2: https://tessl.io/registry/tessl/npm-lexical/0.34.0/files/docs/state-management.md
- 3: https://www.npmjs.com/package/lexical?utm_source=openai
- 4: https://deepwiki.com/facebook/lexical/2-core-architecture?utm_source=openai
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.
| ErrorBoundary={({ children }) => <>{children}</>} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "LexicalEditor.tsx" --type fRepository: fvishw/PMS
Length of output: 102
🏁 Script executed:
sed -n '175,195p' client/src/components/meetings/LexicalEditor.tsx | cat -nRepository: fvishw/PMS
Length of output: 824
🏁 Script executed:
head -50 client/src/components/meetings/LexicalEditor.tsx | cat -nRepository: fvishw/PMS
Length of output: 1935
🏁 Script executed:
rg "LexicalErrorBoundary" --type tsx --type ts -A 2 -B 2Repository: fvishw/PMS
Length of output: 81
🏁 Script executed:
rg "RichTextPlugin" --type tsx --type ts -A 3 -B 1Repository: fvishw/PMS
Length of output: 81
🏁 Script executed:
rg "from [\"']@lexical" --type tsx --type ts | head -20Repository: 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’sErrorBoundary={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:
- 1: https://app.unpkg.com/%40lexical/react%400.27.2/files/LexicalRichTextPlugin.d.ts?utm_source=openai
- 2: https://newreleases.io/project/github/facebook/lexical/release/v0.6.0?utm_source=openai
- 3: https://liveblocks.io/docs/get-started/react-lexical?utm_source=openai
- 4: https://app.unpkg.com/%40lexical/react%400.39.0/files/LexicalErrorBoundary.js.flow?utm_source=openai
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.
| 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.
| 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] | ||
| : "", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| select: (data) => { | ||
| if (data.meeting.notes) { | ||
| setNotesState(data.meeting.notes); | ||
| } | ||
| return data.meeting; | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "MeetingNotesDialog.tsx" --type fRepository: fvishw/PMS
Length of output: 107
🏁 Script executed:
cat -n client/src/components/meetings/MeetingNotesDialog.tsxRepository: fvishw/PMS
Length of output: 3285
🏁 Script executed:
rg "notesState" client/src/components/meetings/MeetingNotesDialog.tsxRepository: fvishw/PMS
Length of output: 188
🏁 Script executed:
rg "MeetingNotesDialog" client/src --type tsx --type ts -A 3 -B 1Repository: fvishw/PMS
Length of output: 81
🏁 Script executed:
rg "MeetingNotesDialog" client/src -A 3 -B 1Repository: fvishw/PMS
Length of output: 2198
🏁 Script executed:
rg "LexicalEditor" client/src/components/meetings/MeetingNotesDialog.tsx -A 5 -B 2Repository: 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.
| {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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "MeetingNotesDialog.tsx" -type fRepository: fvishw/PMS
Length of output: 109
🏁 Script executed:
wc -l ./client/src/components/meetings/MeetingNotesDialog.tsxRepository: fvishw/PMS
Length of output: 112
🏁 Script executed:
cat -n ./client/src/components/meetings/MeetingNotesDialog.tsxRepository: 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.
| editData={{ | ||
| _id: meeting._id, | ||
| title: meeting.title, | ||
| employee: "", | ||
| meetingDate: meeting.meetingDate, |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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 -20Repository: 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.tsxRepository: 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.tsxRepository: 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 -40Repository: 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 -A5Repository: 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.tsxRepository: 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 -A8Repository: 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).
| if (year && year !== "ALL") { | ||
| filter.year = Number(year); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| const { employee } = parsedPayload.data; | ||
| if (!Types.ObjectId.isValid(employee)) { | ||
| throw new ApiError(400, "Invalid employee ID"); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| notes: z.string().optional().default(""), | ||
| status: z.enum(["scheduled", "completed", "cancelled"]).optional(), |
There was a problem hiding this comment.
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.
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
Improvements