Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 396 additions & 11 deletions client/package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@hookform/error-message": "^2.0.1",
"@lexical/html": "^0.41.0",
"@lexical/link": "^0.41.0",
"@lexical/list": "^0.41.0",
"@lexical/react": "^0.41.0",
"@lexical/rich-text": "^0.41.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand All @@ -40,6 +45,7 @@
"dayjs": "^1.11.19",
"input-otp": "^1.4.2",
"jwt-decode": "^4.0.0",
"lexical": "^0.41.0",
"lucide-react": "^0.555.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
Expand Down
44 changes: 42 additions & 2 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
GetUserReport,
GetUserReports,
GetCurrentQuarterStatus,
GetAllMeetings,
GetMeeting,
} from "@/types/apiResponse";
import { CheckInPayload, ICheckInPayload } from "@/types/chekin";
import { Goal } from "@/types/goal";
Expand Down Expand Up @@ -244,8 +246,13 @@ export class API {
markGoalAsComplete(data: ViewGoalFormValues) {
return this.request(this.instance.put("/goals/mark-as-complete", data));
}
getGoalsByOwner(): Promise<GetGoals> {
return this.request(this.instance.get("/goals/get-by-owner"));
getGoalsByOwner(filter?: {
quarter?: string | null;
year?: string | null;
}): Promise<GetGoals> {
return this.request(
this.instance.get("/goals/get-by-owner", { params: filter }),
);
}
deleteGoalById(goalId: string) {
return this.request(
Expand Down Expand Up @@ -288,6 +295,39 @@ export class API {
generateUserReport(): Promise<GetUserReport> {
return this.request(this.instance.post("/reports/generate-user-report"));
}
createMeeting(data: {
title: string;
employee: string;
meetingDate: string;
}) {
return this.request(this.instance.post("/meetings/add", data));
}
updateMeeting(
meetingId: string,
data: { title?: string; meetingDate?: string; notes?: string; status?: string },
) {
return this.request(
this.instance.put(`/meetings/update/${meetingId}`, data),
);
}
deleteMeeting(meetingId: string) {
return this.request(
this.instance.delete("/meetings/delete", { params: { meetingId } }),
);
}
getAllMeetings(filters: {
employeeId?: string | null;
quarter?: string | null;
year?: string | null;
status?: string | null;
}): Promise<GetAllMeetings> {
return this.request(
this.instance.get("/meetings/get-all", { params: filters }),
);
}
getMeetingById(meetingId: string): Promise<GetMeeting> {
return this.request(this.instance.get(`/meetings/get/${meetingId}`));
}
}

const Api = new API();
Expand Down
49 changes: 49 additions & 0 deletions client/src/components/meetings/DeleteMeetingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

interface DeleteMeetingModalProps {
isOpen: boolean;
onClose: () => void;
onConfirmDelete: () => void;
isPending?: boolean;
}

export const DeleteMeetingModal = ({
isOpen,
onClose,
onConfirmDelete,
isPending = false,
}: DeleteMeetingModalProps) => {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Delete Meeting</DialogTitle>
<DialogDescription>
Are you sure you want to delete this meeting? This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isPending}>
Cancel
</Button>
<Button
variant="destructive"
onClick={onConfirmDelete}
disabled={isPending}
>
{isPending ? "Deleting..." : "Delete Meeting"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
195 changes: 195 additions & 0 deletions client/src/components/meetings/LexicalEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useEffect } from "react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { ListItemNode, ListNode } from "@lexical/list";
import { HeadingNode } from "@lexical/rich-text";
import {
$getSelection,
$isRangeSelection,
FORMAT_TEXT_COMMAND,
type EditorState,
type LexicalEditor as LexicalEditorType,
} from "lexical";
import {
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
} from "@lexical/list";
import { useState, useCallback } from "react";
import { Button } from "@/components/ui/button";

const theme = {
paragraph: "mb-1",
text: {
bold: "font-bold",
italic: "italic",
underline: "underline",
},
list: {
nested: {
listitem: "list-none",
},
ol: "list-decimal ml-4",
ul: "list-disc ml-4",
listitem: "ml-2",
},
heading: {
h1: "text-2xl font-bold",
h2: "text-xl font-bold",
h3: "text-lg font-bold",
},
};

function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);

const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
setIsBold(selection.hasFormat("bold"));
setIsItalic(selection.hasFormat("italic"));
setIsUnderline(selection.hasFormat("underline"));
}
}, []);

useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
});
}, [editor, updateToolbar]);

return (
<div className="flex gap-1 border-b p-1">
<Button
type="button"
variant={isBold ? "default" : "ghost"}
size="sm"
className="h-8 w-8 p-0 font-bold"
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
>
B
</Button>
<Button
type="button"
variant={isItalic ? "default" : "ghost"}
size="sm"
className="h-8 w-8 p-0 italic"
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")}
>
I
</Button>
<Button
type="button"
variant={isUnderline ? "default" : "ghost"}
size="sm"
className="h-8 w-8 p-0 underline"
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")}
>
U
</Button>
<div className="mx-1 w-px bg-border" />
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2 text-xs"
onClick={() =>
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
}
>
• List
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2 text-xs"
onClick={() =>
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)
}
>
1. List
</Button>
</div>
);
}

function LoadInitialStatePlugin({
initialState,
}: {
initialState?: string;
}) {
const [editor] = useLexicalComposerContext();
const [loaded, setLoaded] = useState(false);

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]);
Comment on lines +134 to +145

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.


return null;
}

interface LexicalEditorProps {
onChange: (serializedState: string) => void;
initialState?: string;
}

export default function LexicalEditor({
onChange,
initialState,
}: LexicalEditorProps) {
const initialConfig = {
namespace: "MeetingNotesEditor",
theme,
nodes: [ListNode, ListItemNode, HeadingNode],
onError: (error: Error) => {
console.error(error);
},
};

const handleChange = (
editorState: EditorState,
_editor: LexicalEditorType,
) => {
const json = editorState.toJSON();
onChange(JSON.stringify(json));
};

return (
<LexicalComposer initialConfig={initialConfig}>
<div className="rounded-md border">
<ToolbarPlugin />
<div className="relative min-h-[200px]">
<RichTextPlugin
contentEditable={
<ContentEditable className="min-h-[200px] p-3 outline-none" />
}
ErrorBoundary={({ children }) => <>{children}</>}
/>
Comment on lines +185 to +186

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.

</div>
<HistoryPlugin />
<ListPlugin />
<OnChangePlugin onChange={handleChange} />
<LoadInitialStatePlugin initialState={initialState} />
</div>
</LexicalComposer>
);
}
Loading