Overhauled notes system.

This commit is contained in:
Tom Butcher 2025-08-31 21:31:13 +01:00
parent e688bd891d
commit 25743ddc2c
6 changed files with 475 additions and 548 deletions

View 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

View File

@ -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}>

View 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

View File

@ -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>
)

View File

@ -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}>

View 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