Overhauled notes system.
This commit is contained in:
parent
e688bd891d
commit
25743ddc2c
32
src/components/Dashboard/common/MarkdownInput.jsx
Normal file
32
src/components/Dashboard/common/MarkdownInput.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Card, Splitter } from 'antd'
|
||||
import PropTypes from 'prop-types'
|
||||
import CodeBlockEditor from './CodeBlockEditor'
|
||||
import MarkdownDisplay from './MarkdownDisplay'
|
||||
|
||||
const MarkdownInput = ({ value, onChange }) => {
|
||||
return (
|
||||
<Splitter className={'farmcontrol-splitter'} style={{ height: '100%' }}>
|
||||
<Splitter.Panel>
|
||||
<Card>
|
||||
<CodeBlockEditor
|
||||
code={value}
|
||||
onChange={onChange}
|
||||
language='markdown'
|
||||
/>
|
||||
</Card>
|
||||
</Splitter.Panel>
|
||||
<Splitter.Panel>
|
||||
<Card style={{ height: '100%' }}>
|
||||
<MarkdownDisplay content={value} />
|
||||
</Card>
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
)
|
||||
}
|
||||
|
||||
MarkdownInput.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func
|
||||
}
|
||||
|
||||
export default MarkdownInput
|
||||
@ -13,6 +13,7 @@ const NewObjectButtons = ({
|
||||
}) => {
|
||||
return (
|
||||
<Flex justify='end'>
|
||||
{totalSteps > 1 ? (
|
||||
<Button
|
||||
style={{ margin: '0 8px' }}
|
||||
onClick={onPrevious}
|
||||
@ -20,6 +21,7 @@ const NewObjectButtons = ({
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{currentStep < totalSteps - 1 ? (
|
||||
<Button type='primary' disabled={!formValid} onClick={onNext}>
|
||||
|
||||
313
src/components/Dashboard/common/NoteItem.jsx
Normal file
313
src/components/Dashboard/common/NoteItem.jsx
Normal file
@ -0,0 +1,313 @@
|
||||
import { useState, useContext, useCallback, useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Flex,
|
||||
Tag,
|
||||
Dropdown,
|
||||
Divider,
|
||||
Modal
|
||||
} 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 IdDisplay from './IdDisplay'
|
||||
import MissingPlaceholder from './MissingPlaceholder'
|
||||
import NewNote from '../Management/Notes/NewNote'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const NoteItem = ({ note }) => {
|
||||
const [childNotes, setChildNotes] = useState([])
|
||||
const noteModel = getModelByName('note')
|
||||
const infoAction = noteModel.actions.filter(
|
||||
(action) => action.name == 'info'
|
||||
)[0]
|
||||
const InfoIcon = infoAction.icon
|
||||
const [newNoteOpen, setNewNoteOpen] = useState(false)
|
||||
const [deleteNoteOpen, setDeleteNoteOpen] = useState(false)
|
||||
const [deleteNoteLoading, setDeleteNoteLoading] = useState(false)
|
||||
const [childNotesLoading, setChildNotesLoading] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const subscribeToObjectTypeUpdatesRef = useRef(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const {
|
||||
deleteObject,
|
||||
fetchObjects,
|
||||
subscribeToObjectTypeUpdates,
|
||||
connected
|
||||
} = useContext(ApiServerContext)
|
||||
const { userProfile, token } = useContext(AuthContext)
|
||||
|
||||
let transformValue = 'rotate(0deg)'
|
||||
if (isExpanded) {
|
||||
transformValue = 'rotate(-90deg)'
|
||||
}
|
||||
|
||||
const handleNoteExpand = useCallback(async () => {
|
||||
setChildNotesLoading(true)
|
||||
try {
|
||||
const childNotesData = await fetchObjects('note', {
|
||||
filter: { 'parent._id': note._id }
|
||||
})
|
||||
setChildNotes(childNotesData.data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching child notes:', error)
|
||||
} finally {
|
||||
setChildNotesLoading(false)
|
||||
}
|
||||
}, [note._id, fetchObjects])
|
||||
|
||||
const toggleExpand = async () => {
|
||||
if (isExpanded == false) {
|
||||
await handleNoteExpand()
|
||||
setIsExpanded(true)
|
||||
} else {
|
||||
setChildNotes([])
|
||||
setIsExpanded(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteNote = async () => {
|
||||
if (token != null) {
|
||||
setDeleteNoteLoading(true)
|
||||
await deleteObject(note._id, 'note')
|
||||
setDeleteNoteOpen(false)
|
||||
setDeleteNoteLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (connected == true && subscribeToObjectTypeUpdatesRef.current == null) {
|
||||
if (isExpanded == true) {
|
||||
subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
|
||||
'note',
|
||||
(noteData) => {
|
||||
if (noteData.parent._id == note._id) {
|
||||
console.log(
|
||||
'Note note added to parent:',
|
||||
note._id,
|
||||
'isExpanded:',
|
||||
isExpanded
|
||||
)
|
||||
if (isExpanded == true) {
|
||||
handleNoteExpand()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (subscribeToObjectTypeUpdatesRef.current) {
|
||||
subscribeToObjectTypeUpdatesRef.current()
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (connected == true && subscribeToObjectTypeUpdatesRef.current) {
|
||||
subscribeToObjectTypeUpdatesRef.current()
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [
|
||||
subscribeToObjectTypeUpdates,
|
||||
connected,
|
||||
handleNoteExpand,
|
||||
isExpanded,
|
||||
note._id
|
||||
])
|
||||
|
||||
// Check if the current user can delete this note
|
||||
const canDeleteNote = userProfile && userProfile._id === note.user._id
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
key: 'new',
|
||||
icon: <PlusIcon />,
|
||||
label: 'New Note',
|
||||
onClick: () => {
|
||||
setNewNoteOpen(true)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Only add delete option if user owns the note
|
||||
if (canDeleteNote) {
|
||||
dropdownItems.push({
|
||||
key: 'delete',
|
||||
label: 'Delete Note',
|
||||
icon: <BinIcon />,
|
||||
onClick: () => {
|
||||
setDeleteNoteOpen(true)
|
||||
},
|
||||
danger: true
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={note._id}
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: note.noteType.color + '26',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
<Flex vertical gap={'small'}>
|
||||
<Flex gap={'middle'} align='start'>
|
||||
<Space>
|
||||
<PersonIcon />
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>{note.user.name}:</Text>
|
||||
</Space>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<MarkdownDisplay content={note.content} />
|
||||
</div>
|
||||
</Flex>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Flex wrap gap={'small'}>
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['hover']}
|
||||
placement='bottomLeft'
|
||||
>
|
||||
<Button size='small'>Actions</Button>
|
||||
</Dropdown>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Type:</Text>
|
||||
<Tag color={note.noteType.color} style={{ margin: 0 }}>
|
||||
{note.noteType.name}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>User ID:</Text>
|
||||
<IdDisplay
|
||||
longId={false}
|
||||
id={note.user._id}
|
||||
type={'user'}
|
||||
showHyperlink={true}
|
||||
/>
|
||||
</Space>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Created At:</Text>
|
||||
<TimeDisplay dateTime={note.createdAt} showSince={true} />
|
||||
</Space>
|
||||
<Flex style={{ flexGrow: 1 }} justify='end'>
|
||||
<Space size={'small'}>
|
||||
<Button
|
||||
icon={<InfoIcon />}
|
||||
type='text'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
navigate(infoAction.url(note._id))
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={
|
||||
childNotesLoading ? <LoadingOutlined /> : <CaretLeftFilled />
|
||||
}
|
||||
size='small'
|
||||
type='text'
|
||||
loading={childNotesLoading}
|
||||
disabled={childNotesLoading}
|
||||
style={{
|
||||
transform: transformValue,
|
||||
transition: 'transform 0.2s ease'
|
||||
}}
|
||||
onClick={toggleExpand}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{isExpanded && (
|
||||
<Flex vertical gap={'small'} style={{ flexGrow: 1 }}>
|
||||
{childNotes.length > 0 ? (
|
||||
childNotes.map((childNote) => (
|
||||
<NoteItem key={childNote._id} note={childNote} />
|
||||
))
|
||||
) : (
|
||||
<MissingPlaceholder message={'No child notes.'} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex vertical gap={'middle'}></Flex>
|
||||
</Flex>
|
||||
<Modal
|
||||
open={newNoteOpen}
|
||||
onCancel={() => {
|
||||
setNewNoteOpen(false)
|
||||
}}
|
||||
width={800}
|
||||
closeIcon={false}
|
||||
destroyOnHidden={true}
|
||||
footer={null}
|
||||
>
|
||||
<NewNote
|
||||
onOk={() => {
|
||||
setNewNoteOpen(false)
|
||||
}}
|
||||
defaultValues={{
|
||||
parent: { _id: note._id },
|
||||
parentType: 'note'
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={deleteNoteOpen}
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
<ExclamationOctagonIcon />
|
||||
Confirm Delete
|
||||
</Space>
|
||||
}
|
||||
okText='Delete'
|
||||
cancelText='Cancel'
|
||||
okType='danger'
|
||||
closable={false}
|
||||
centered
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button
|
||||
key='cancel'
|
||||
onClick={() => {
|
||||
setDeleteNoteOpen(false)
|
||||
}}
|
||||
disabled={deleteNoteLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key='delete'
|
||||
type='primary'
|
||||
danger
|
||||
onClick={handleDeleteNote}
|
||||
loading={deleteNoteLoading}
|
||||
disabled={deleteNoteLoading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Text>Are you sure you want to delete this note?</Text>
|
||||
</Modal>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
NoteItem.propTypes = {
|
||||
note: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
export default NoteItem
|
||||
@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { useCallback, useContext, useEffect, useState, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Card,
|
||||
@ -7,292 +7,35 @@ import {
|
||||
Typography,
|
||||
Flex,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Switch,
|
||||
Spin,
|
||||
Alert,
|
||||
message,
|
||||
Divider,
|
||||
Tag,
|
||||
Dropdown
|
||||
} from 'antd'
|
||||
import { CaretLeftFilled, LoadingOutlined } from '@ant-design/icons'
|
||||
import { 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 { ApiServerContext } from '../context/ApiServerContext'
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
import IdDisplay from './IdDisplay'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||
import ObjectProperty from './ObjectProperty'
|
||||
import NewNote from '../Management/Notes/NewNote'
|
||||
import NoteItem from './NoteItem'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { TextArea } = Input
|
||||
const { Text } = Typography
|
||||
|
||||
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 = useCallback(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)
|
||||
}
|
||||
}, [fetchData, note._id])
|
||||
|
||||
// Listen for child note additions
|
||||
useEffect(() => {
|
||||
if (onChildNoteAdded) {
|
||||
onChildNoteAdded(note._id, reloadChildNotes)
|
||||
}
|
||||
}, [note._id, onChildNoteAdded, reloadChildNotes])
|
||||
|
||||
// Check if the current user can delete this note
|
||||
const canDeleteNote = userProfile && userProfile._id === note.user._id
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
key: 'new',
|
||||
icon: <PlusIcon />,
|
||||
label: 'New Note',
|
||||
onClick: handleNewChildNote
|
||||
}
|
||||
]
|
||||
|
||||
// Only add delete option if user owns the note
|
||||
if (canDeleteNote) {
|
||||
dropdownItems.push({
|
||||
key: 'delete',
|
||||
label: 'Delete Note',
|
||||
icon: <BinIcon />,
|
||||
onClick: handleDeleteNote,
|
||||
danger: true
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={note._id}
|
||||
size='small'
|
||||
style={{
|
||||
backgroundColor: note.noteType.color + '26',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
>
|
||||
<Flex vertical gap={'small'}>
|
||||
<Flex gap={'middle'} align='start'>
|
||||
<Space>
|
||||
<PersonIcon />
|
||||
<Text style={{ whiteSpace: 'nowrap' }}>{note.user.name}:</Text>
|
||||
</Space>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<MarkdownDisplay content={note.content} />
|
||||
</div>
|
||||
</Flex>
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Flex wrap gap={'small'}>
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['hover']}
|
||||
placement='bottomLeft'
|
||||
>
|
||||
<Button size='small'>Actions</Button>
|
||||
</Dropdown>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Type:</Text>
|
||||
<Tag color={note.noteType.color} style={{ margin: 0 }}>
|
||||
{note.noteType.name}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>User ID:</Text>
|
||||
<IdDisplay
|
||||
longId={false}
|
||||
id={note.user._id}
|
||||
type={'user'}
|
||||
showHyperlink={true}
|
||||
/>
|
||||
</Space>
|
||||
<Space size={'small'} style={{ marginRight: 8 }}>
|
||||
<Text type='secondary'>Created At:</Text>
|
||||
<TimeDisplay dateTime={note.createdAt} showSince={true} />
|
||||
</Space>
|
||||
<Flex style={{ flexGrow: 1 }} justify='end'>
|
||||
<Space size={'small'}>
|
||||
<Button
|
||||
icon={
|
||||
isThisNoteLoading ? <LoadingOutlined /> : <CaretLeftFilled />
|
||||
}
|
||||
size='small'
|
||||
type='text'
|
||||
loading={isThisNoteLoading}
|
||||
disabled={isThisNoteLoading}
|
||||
style={{
|
||||
transform: transformValue,
|
||||
transition: 'transform 0.2s ease'
|
||||
}}
|
||||
onClick={() => handleNoteExpand(note._id)}
|
||||
/>
|
||||
</Space>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{isExpanded && (
|
||||
<>
|
||||
<Flex vertical gap={'small'} style={{ flexGrow: 1 }}>
|
||||
{hasChildNotes ? (
|
||||
childNotes[note._id].map((childNote) => (
|
||||
<NoteItem
|
||||
key={childNote._id}
|
||||
note={childNote}
|
||||
expandedNotes={expandedNotes}
|
||||
setExpandedNotes={setExpandedNotes}
|
||||
fetchData={fetchData}
|
||||
onNewNote={onNewNote}
|
||||
onDeleteNote={onDeleteNote}
|
||||
userProfile={userProfile}
|
||||
onChildNoteAdded={onChildNoteAdded}
|
||||
/>
|
||||
))
|
||||
) : !isThisNoteLoading ? (
|
||||
<Card size='small'>
|
||||
<Flex
|
||||
justify='center'
|
||||
gap={'small'}
|
||||
style={{ height: '100%' }}
|
||||
align='center'
|
||||
>
|
||||
<Text type='secondary'>
|
||||
<InfoCircleIcon />
|
||||
</Text>
|
||||
<Text type='secondary'>No child notes.</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : null}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
<Flex vertical gap={'middle'}></Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
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, type }) => {
|
||||
const NotesPanel = ({ _id, type }) => {
|
||||
const [newNoteOpen, setNewNoteOpen] = useState(false)
|
||||
const [showMarkdown, setShowMarkdown] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
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 [selectedParentType, setSelectedParentType] = useState(null)
|
||||
const [childNoteCallbacks, setChildNoteCallbacks] = useState({})
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
const [noteToDelete, setNoteToDelete] = useState(null)
|
||||
const subscribeToObjectTypeUpdatesRef = useRef(null)
|
||||
|
||||
const newNoteFormUpdateValues = Form.useWatch([], newNoteForm)
|
||||
|
||||
useEffect(() => {
|
||||
newNoteForm
|
||||
.validateFields({
|
||||
validateOnly: true
|
||||
})
|
||||
.then(() => setDoneEnabled(true))
|
||||
.catch(() => setDoneEnabled(false))
|
||||
}, [newNoteForm, newNoteFormUpdateValues])
|
||||
|
||||
const { token, userProfile } = useContext(AuthContext)
|
||||
const { fetchNotes } = useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
const { fetchNotes, connected, subscribeToObjectTypeUpdates } =
|
||||
useContext(ApiServerContext)
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (id) => {
|
||||
@ -307,17 +50,6 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
[fetchNotes]
|
||||
)
|
||||
|
||||
const handleNewChildNote = useCallback(
|
||||
(parentId) => {
|
||||
setSelectedParentId(parentId)
|
||||
setSelectedParentType('note')
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
},
|
||||
[newNoteForm]
|
||||
)
|
||||
|
||||
const generateNotes = useCallback(
|
||||
async (id) => {
|
||||
const notesData = await fetchData(id)
|
||||
@ -351,114 +83,37 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
note={note}
|
||||
expandedNotes={expandedNotes}
|
||||
setExpandedNotes={setExpandedNotes}
|
||||
fetchData={fetchData}
|
||||
onNewNote={handleNewChildNote}
|
||||
onDeleteNote={handleDeleteNote}
|
||||
userProfile={userProfile}
|
||||
onChildNoteAdded={(noteId, callback) => {
|
||||
setChildNoteCallbacks((prev) => ({
|
||||
...prev,
|
||||
[noteId]: callback
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
))
|
||||
},
|
||||
[fetchData, expandedNotes, userProfile, handleNewChildNote]
|
||||
[fetchData, expandedNotes]
|
||||
)
|
||||
|
||||
const handleNewNote = async () => {
|
||||
setNewNoteFormLoading(true)
|
||||
try {
|
||||
await axios.post(
|
||||
`${config.backendUrl}/notes`,
|
||||
{
|
||||
...newNoteFormValues,
|
||||
parent: selectedParentId,
|
||||
parentType: selectedParentType
|
||||
},
|
||||
{
|
||||
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 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 () => {
|
||||
console.log('GOT RELOAD DATA')
|
||||
setNotes(await generateNotes(_id))
|
||||
}, [_id, generateNotes])
|
||||
|
||||
useEffect(() => {
|
||||
if (connected == true && subscribeToObjectTypeUpdatesRef.current == null) {
|
||||
subscribeToObjectTypeUpdatesRef.current = subscribeToObjectTypeUpdates(
|
||||
'note',
|
||||
(noteData) => {
|
||||
if (noteData.parent._id == _id) {
|
||||
console.log('Note note added to parent:', _id)
|
||||
handleReloadData()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
if (connected == true && subscribeToObjectTypeUpdatesRef.current) {
|
||||
subscribeToObjectTypeUpdatesRef.current()
|
||||
subscribeToObjectTypeUpdatesRef.current = null
|
||||
}
|
||||
}
|
||||
}, [_id, subscribeToObjectTypeUpdates, connected, handleReloadData])
|
||||
|
||||
useEffect(() => {
|
||||
if (token != null && !initialized) {
|
||||
handleReloadData()
|
||||
@ -466,25 +121,6 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
}
|
||||
}, [token, handleReloadData, initialized])
|
||||
|
||||
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: [
|
||||
{
|
||||
@ -504,11 +140,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
setLoading(true)
|
||||
handleReloadData()
|
||||
} else if (key === 'newNote') {
|
||||
setSelectedParentId(_id)
|
||||
setSelectedParentType(type)
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -528,11 +160,7 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
icon={<PlusIcon />}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setSelectedParentId(_id)
|
||||
setSelectedParentType(type)
|
||||
setNewNoteOpen(true)
|
||||
newNoteForm.resetFields()
|
||||
setNewNoteFormValues({})
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
@ -552,146 +180,24 @@ const NotesPanel = ({ _id, onNewNote, type }) => {
|
||||
|
||||
<Modal
|
||||
open={newNoteOpen}
|
||||
onOk={handleModalOk}
|
||||
onCancel={handleModalCancel}
|
||||
width={800}
|
||||
closeIcon={false}
|
||||
destroyOnHidden={true}
|
||||
footer={false}
|
||||
>
|
||||
<Flex vertical gap='large'>
|
||||
<Flex vertical gap='middle'>
|
||||
<Flex align='center' justify='space-between'>
|
||||
<Title level={2} style={{ marginTop: 0, marginBottom: 4 }}>
|
||||
New Note
|
||||
</Title>
|
||||
<Space gap={'small'}>
|
||||
<Text type='secondary'>Markdown:</Text>
|
||||
<Switch onChange={setShowMarkdown} size='small' />
|
||||
</Space>
|
||||
</Flex>
|
||||
<Form
|
||||
form={newNoteForm}
|
||||
layout='vertical'
|
||||
onFinish={handleNewNote}
|
||||
initialValues={{ content: '' }}
|
||||
onValuesChange={(changedValues) =>
|
||||
setNewNoteFormValues((prevValues) => ({
|
||||
...prevValues,
|
||||
...changedValues
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Flex vertical gap={'large'}>
|
||||
<Flex gap='middle' wrap>
|
||||
<Form.Item
|
||||
name='content'
|
||||
rules={[{ required: true, message: '' }]}
|
||||
style={{ margin: 0, flexGrow: 1, minWidth: '300px' }}
|
||||
>
|
||||
<TextArea
|
||||
rows={6}
|
||||
placeholder='Enter note content'
|
||||
style={{ resize: 'none' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{showMarkdown && (
|
||||
<Card
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minWidth: '300px',
|
||||
backgroundColor: () => {
|
||||
if (newNoteFormValues?.noteType?.color) {
|
||||
return newNoteFormValues.noteType.color + '26'
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MarkdownDisplay
|
||||
content={newNoteForm.getFieldValue('content') || ''}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
<Form.Item
|
||||
name='noteType'
|
||||
style={{ margin: 0 }}
|
||||
rules={[
|
||||
{ required: true, message: 'Please select a note type' }
|
||||
]}
|
||||
>
|
||||
<ObjectProperty
|
||||
type='object'
|
||||
objectType='noteType'
|
||||
isEditing={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Flex>
|
||||
<Flex justify='end'>
|
||||
<Button
|
||||
style={{ margin: '0 8px' }}
|
||||
disabled={newNoteFormLoading}
|
||||
onClick={() => {
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
setNewNoteOpen(false)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='primary'
|
||||
loading={newNoteFormLoading}
|
||||
onClick={() => {
|
||||
newNoteForm.submit()
|
||||
<NewNote
|
||||
onOk={() => {
|
||||
setNewNoteOpen(false)
|
||||
messageApi.success('New note added.')
|
||||
}}
|
||||
disabled={newNoteFormLoading || !doneEnabled}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={deleteConfirmOpen}
|
||||
title={
|
||||
<Space size={'middle'}>
|
||||
<ExclamationOctagonIcon />
|
||||
Confirm Delete
|
||||
</Space>
|
||||
}
|
||||
onOk={confirmDeleteNote}
|
||||
onCancel={cancelDeleteNote}
|
||||
okText='Delete'
|
||||
cancelText='Cancel'
|
||||
okType='danger'
|
||||
closable={false}
|
||||
centered
|
||||
maskClosable={false}
|
||||
footer={[
|
||||
<Button
|
||||
key='cancel'
|
||||
onClick={cancelDeleteNote}
|
||||
disabled={deleteNoteLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key='delete'
|
||||
type='primary'
|
||||
danger
|
||||
onClick={confirmDeleteNote}
|
||||
loading={deleteNoteLoading}
|
||||
disabled={deleteNoteLoading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Text>Are you sure you want to delete this note?</Text>
|
||||
reset={newNoteOpen}
|
||||
defaultValues={{
|
||||
parent: { _id },
|
||||
parentType: type
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@ -31,6 +31,7 @@ import ObjectList from './ObjectList'
|
||||
import VarianceDisplay from './VarianceDisplay'
|
||||
import OperationDisplay from './OperationDisplay'
|
||||
import MarkdownDisplay from './MarkdownDisplay'
|
||||
import MarkdownInput from './MarkdownInput'
|
||||
import ObjectSelect from './ObjectSelect'
|
||||
import ObjectDisplay from './ObjectDisplay'
|
||||
import ObjectTypeSelect from './ObjectTypeSelect'
|
||||
@ -603,6 +604,12 @@ const ObjectProperty = ({
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
case 'markdown':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
<MarkdownInput value={value} />
|
||||
</Form.Item>
|
||||
)
|
||||
case 'material':
|
||||
return (
|
||||
<Form.Item name={formItemName} {...mergedFormItemProps}>
|
||||
|
||||
67
src/components/Dashboard/common/WizardView.jsx
Normal file
67
src/components/Dashboard/common/WizardView.jsx
Normal file
@ -0,0 +1,67 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useState } from 'react'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { Typography, Flex, Steps, Divider } from 'antd'
|
||||
import NewObjectButtons from './NewObjectButtons'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const WizardView = ({
|
||||
showSteps = true,
|
||||
steps,
|
||||
title = 'Wizard View',
|
||||
onSubmit,
|
||||
formValid,
|
||||
loading
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
return (
|
||||
<Flex gap='middle'>
|
||||
{!isMobile && showSteps == true ? (
|
||||
<div style={{ minWidth: '160px' }}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={steps}
|
||||
direction='vertical'
|
||||
style={{ width: 'fit-content' }}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isMobile && showSteps == true ? (
|
||||
<Divider type='vertical' style={{ height: 'unset' }} />
|
||||
) : null}
|
||||
|
||||
<Flex vertical gap='middle' style={{ flexGrow: 1 }}>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
{title}
|
||||
</Title>
|
||||
<div style={{ minHeight: '260px', marginBottom: 8 }}>
|
||||
{steps[currentStep].content}
|
||||
</div>
|
||||
<NewObjectButtons
|
||||
currentStep={currentStep}
|
||||
totalSteps={steps.length}
|
||||
onPrevious={() => setCurrentStep((prev) => prev - 1)}
|
||||
onNext={() => setCurrentStep((prev) => prev + 1)}
|
||||
onSubmit={onSubmit}
|
||||
formValid={formValid}
|
||||
submitLoading={loading}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
WizardView.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
formValid: PropTypes.bool.isRequired,
|
||||
steps: PropTypes.array.isRequired,
|
||||
showSteps: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
loading: PropTypes.bool
|
||||
}
|
||||
|
||||
export default WizardView
|
||||
Loading…
x
Reference in New Issue
Block a user