import React, { useCallback, useContext, useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import {
Card,
Button,
Space,
Typography,
Flex,
Modal,
Form,
Input,
Switch,
Spin,
Alert,
message,
Divider,
Tag,
Dropdown
} from 'antd'
import { CaretLeftFilled, LoadingOutlined } from '@ant-design/icons'
import PlusIcon from '../../Icons/PlusIcon'
import BinIcon from '../../Icons/BinIcon'
import PersonIcon from '../../Icons/PersonIcon'
import TimeDisplay from './TimeDisplay'
import MarkdownDisplay from './MarkdownDisplay'
import axios from 'axios'
import config from '../../../config'
import { AuthContext } from '../context/AuthContext'
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
import NoteTypeSelect from './NoteTypeSelect'
import IdDisplay from './IdDisplay'
import ReloadIcon from '../../Icons/ReloadIcon'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
const { Text, Title } = Typography
const { TextArea } = Input
const NoteItem = ({
note,
expandedNotes,
setExpandedNotes,
fetchData,
onNewNote,
onDeleteNote,
userProfile,
onChildNoteAdded
}) => {
const [childNotes, setChildNotes] = useState({})
const [loadingChildNotes, setLoadingChildNotes] = useState(null)
const isExpanded = expandedNotes[note._id]
const hasChildNotes = childNotes[note._id] && childNotes[note._id].length > 0
const isThisNoteLoading = loadingChildNotes === note._id
let transformValue = 'rotate(0deg)'
if (isExpanded) {
transformValue = 'rotate(-90deg)'
}
const handleNoteExpand = async (noteId) => {
const newExpandedState = !expandedNotes[noteId]
setExpandedNotes((prev) => ({
...prev,
[noteId]: newExpandedState
}))
if (newExpandedState && !childNotes[noteId]) {
setLoadingChildNotes(noteId)
try {
const childNotesData = await fetchData(noteId)
setChildNotes((prev) => ({
...prev,
[noteId]: childNotesData
}))
} catch (error) {
console.error('Error fetching child notes:', error)
} finally {
setLoadingChildNotes(null)
}
}
}
const handleNewChildNote = () => {
if (onNewNote) {
onNewNote(note._id)
}
}
const handleDeleteNote = () => {
if (onDeleteNote) {
onDeleteNote(note._id)
}
}
// Reload child notes when a new child note is added
const reloadChildNotes = async () => {
// Always fetch child notes when this function is called
// This ensures child notes are loaded even if the parent wasn't expanded before
setLoadingChildNotes(note._id)
try {
const childNotesData = await fetchData(note._id)
setChildNotes((prev) => ({
...prev,
[note._id]: childNotesData
}))
} catch (error) {
console.error('Error fetching child notes:', error)
} finally {
setLoadingChildNotes(null)
}
}
// Listen for child note additions
useEffect(() => {
if (onChildNoteAdded) {
onChildNoteAdded(note._id, reloadChildNotes)
}
}, [note._id, onChildNoteAdded])
// Check if the current user can delete this note
const canDeleteNote = userProfile && userProfile._id === note.user._id
const dropdownItems = [
{
key: 'new',
icon: ,
label: 'New Note',
onClick: handleNewChildNote
}
]
// Only add delete option if user owns the note
if (canDeleteNote) {
dropdownItems.push({
key: 'delete',
label: 'Delete Note',
icon: ,
onClick: handleDeleteNote,
danger: true
})
}
return (
{note.user.name}:
Type:
{note.noteType.name}
User ID:
Created At:
:
}
size='small'
type='text'
loading={isThisNoteLoading}
disabled={isThisNoteLoading}
style={{
transform: transformValue,
transition: 'transform 0.2s ease'
}}
onClick={() => handleNoteExpand(note._id)}
/>
{isExpanded && (
<>
{hasChildNotes ? (
childNotes[note._id].map((childNote) => (
))
) : !isThisNoteLoading ? (
No child notes.
) : null}
>
)}
)
}
NoteItem.propTypes = {
note: PropTypes.object.isRequired,
expandedNotes: PropTypes.object.isRequired,
setExpandedNotes: PropTypes.func.isRequired,
fetchData: PropTypes.func.isRequired,
onNewNote: PropTypes.func,
onDeleteNote: PropTypes.func,
userProfile: PropTypes.object,
onChildNoteAdded: PropTypes.func
}
const NotesPanel = ({ _id, onNewNote }) => {
const [newNoteOpen, setNewNoteOpen] = useState(false)
const [showMarkdown, setShowMarkdown] = useState(false)
const [loading, setLoading] = useState(true)
const [messageApi, contextHolder] = message.useMessage()
const [newNoteFormLoading, setNewNoteFormLoading] = useState(false)
const [newNoteFormValues, setNewNoteFormValues] = useState({})
const [deleteNoteLoading, setDeleteNoteLoading] = useState(false)
const [doneEnabled, setDoneEnabled] = useState(false)
const [error, setError] = useState(null)
const [notes, setNotes] = useState(null)
const [expandedNotes, setExpandedNotes] = useState({})
const [newNoteForm] = Form.useForm()
const [selectedParentId, setSelectedParentId] = useState(null)
const [childNoteCallbacks, setChildNoteCallbacks] = useState({})
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [noteToDelete, setNoteToDelete] = useState(null)
const newNoteFormUpdateValues = Form.useWatch([], newNoteForm)
React.useEffect(() => {
newNoteForm
.validateFields({
validateOnly: true
})
.then(() => setDoneEnabled(true))
.catch(() => setDoneEnabled(false))
}, [newNoteForm, newNoteFormUpdateValues])
const { authenticated, userProfile } = useContext(AuthContext)
const fetchData = useCallback(async (id) => {
try {
const response = await axios.get(`${config.backendUrl}/notes`, {
params: {
parent: id,
sort: 'createdAt',
order: 'ascend'
},
headers: {
Accept: 'application/json'
},
withCredentials: true
})
const newData = response.data
setLoading(false)
return newData
} catch (error) {
setNotes([])
setError(error)
setLoading(false)
}
}, [])
const generateNotes = useCallback(
async (id) => {
const notesData = await fetchData(id)
if (notesData.length <= 0) {
return (
} spinning={loading}>
No notes added.
)
}
return notesData.map((note) => (
{
setChildNoteCallbacks((prev) => ({
...prev,
[noteId]: callback
}))
}}
/>
))
},
[loading, fetchData, expandedNotes, userProfile]
)
const handleNewNote = async () => {
setNewNoteFormLoading(true)
try {
await axios.post(
`${config.backendUrl}/notes`,
{ ...newNoteFormValues, parent: selectedParentId || _id },
{
withCredentials: true
}
)
setNewNoteOpen(false)
messageApi.success('Added a new note.')
// If this is a child note, expand the parent and reload child notes
if (selectedParentId) {
// Ensure parent is expanded
setExpandedNotes((prev) => ({
...prev,
[selectedParentId]: true
}))
// Add a small delay to ensure state update has taken effect
setTimeout(() => {
// Reload child notes for the parent
if (childNoteCallbacks[selectedParentId]) {
childNoteCallbacks[selectedParentId]()
}
}, 100)
} else {
// If it's a top-level note, reload all notes
setLoading(true)
handleReloadData()
}
setSelectedParentId(null)
} catch (error) {
messageApi.error('Error creating new note: ' + error.message)
} finally {
setNewNoteFormLoading(false)
}
}
const handleNewNoteFromDropdown = (parentId) => {
setSelectedParentId(parentId)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
}
const handleDeleteNote = async (noteId) => {
setNoteToDelete(noteId)
setDeleteConfirmOpen(true)
}
const confirmDeleteNote = async () => {
if (!noteToDelete) return
setDeleteNoteLoading(true)
try {
await axios.delete(`${config.backendUrl}/notes/${noteToDelete}`, {
withCredentials: true
})
messageApi.success('Note deleted successfully.')
// Reload all top-level notes
setLoading(true)
handleReloadData()
// Reload child notes for all expanded parents to ensure UI stays in sync
const expandedNoteIds = Object.keys(expandedNotes).filter(
(id) => expandedNotes[id]
)
for (const parentId of expandedNoteIds) {
if (childNoteCallbacks[parentId]) {
childNoteCallbacks[parentId]()
}
}
} catch (error) {
messageApi.error('Error deleting note: ' + error.message)
} finally {
setDeleteNoteLoading(false)
setDeleteConfirmOpen(false)
setNoteToDelete(null)
}
}
const cancelDeleteNote = () => {
setDeleteConfirmOpen(false)
setNoteToDelete(null)
}
const handleReloadData = useCallback(async () => {
setNotes(await generateNotes(_id))
}, [_id, generateNotes])
useEffect(() => {
if (authenticated) {
handleReloadData()
}
}, [authenticated, handleReloadData])
const handleModalOk = async () => {
try {
const values = await newNoteForm.validateFields()
onNewNote(values)
newNoteForm.resetFields()
setNewNoteOpen(false)
setShowMarkdown(false)
} catch (error) {
console.error('Validation failed:', error)
}
}
const handleModalCancel = () => {
newNoteForm.resetFields()
setNewNoteOpen(false)
setShowMarkdown(false)
setSelectedParentId(null)
}
const actionItems = {
items: [
{
label: 'New Note',
key: 'newNote',
icon:
},
{ type: 'divider' },
{
label: 'Reload Notes',
key: 'reloadNotes',
icon:
}
],
onClick: ({ key }) => {
if (key === 'reloadNotes') {
setLoading(true)
handleReloadData()
} else if (key === 'newNote') {
setSelectedParentId(null)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
}
}
}
return (
{contextHolder}
}
disabled={loading}
onClick={() => {
setSelectedParentId(null)
setNewNoteOpen(true)
newNoteForm.resetFields()
setNewNoteFormValues({})
}}
/>
} spinning={loading}>
{error ? (
) : (
{notes}
)}
New Note
Markdown:
Confirm Delete
}
onOk={confirmDeleteNote}
onCancel={cancelDeleteNote}
okText='Delete'
cancelText='Cancel'
okType='danger'
closable={false}
centered
maskClosable={false}
footer={[
,
]}
>
Are you sure you want to delete this note?
)
}
NotesPanel.propTypes = {
_id: PropTypes.string.isRequired,
onNewNote: PropTypes.func
}
export default NotesPanel