feat(annotations): comments, threads, voting system with review mode
Backend: - comment_threads + comments + object_votes tables with indexes - Thread/comment CRUD endpoints with socket broadcast - Vote toggle endpoint with socket broadcast Frontend: - AnnotationStore for reactive thread/comment/vote state - FeedbackPanel with thread list, expanded view, replies, filtering - Vote toggle buttons in panel - PinOverlay for canvas pin markers + vote badges - Review Mode toggle in toolbar (comment bubble icon) - Jump-to-object from thread view - Orphaned thread detection for deleted objects - New comment creation from panel when object selected - Socket event wiring for real-time sync
This commit is contained in:
@@ -26,6 +26,7 @@ import MattermostImport from '../components/MattermostImport';
|
||||
import Minimap from '../components/Minimap';
|
||||
import UploadPanel from '../components/UploadPanel';
|
||||
import ExportDialog from '../components/ExportDialog';
|
||||
import FeedbackPanel from '../components/FeedbackPanel';
|
||||
import { UploadManager } from '../stores/uploadManager';
|
||||
import { InboxZone } from '../canvas/InboxZone';
|
||||
import { getItemWorldBounds } from '../canvas/SceneManager';
|
||||
@@ -90,6 +91,7 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showMmImport, setShowMmImport] = useState(false);
|
||||
const [showExport, setShowExport] = useState(false);
|
||||
const [reviewMode, setReviewMode] = useState(false);
|
||||
const [focusMode, setFocusMode] = useState(false);
|
||||
const [layerList, setLayerList] = useState<any[]>([]);
|
||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||
@@ -153,13 +155,21 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
getViewport,
|
||||
});
|
||||
|
||||
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox)
|
||||
useCanvasSetup({
|
||||
// Canvas setup (selection, undo, sync, socket, drag/drop, paste, inbox, annotations)
|
||||
const { annotationStore, pinOverlay } = useCanvasSetup({
|
||||
boardData, resolvedBoardId, user, isPublicView,
|
||||
canvasRef, selectionRef, undoRef, syncRef, inboxZoneRef, canvasContainerRef,
|
||||
uploadManager, onCanvasChange, showToast, setOnlineUsers, setSelectedLayerIds,
|
||||
});
|
||||
|
||||
// Toggle pin overlay visibility with review mode
|
||||
useEffect(() => {
|
||||
if (pinOverlay) {
|
||||
pinOverlay.visible = reviewMode;
|
||||
if (reviewMode) pinOverlay.refresh();
|
||||
}
|
||||
}, [reviewMode, pinOverlay]);
|
||||
|
||||
// Tool activation
|
||||
useEffect(() => {
|
||||
const viewport = canvasRef.current?.getViewport();
|
||||
@@ -490,6 +500,8 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
onToggleHelp={() => setShowHelp((v) => !v)}
|
||||
onMmImport={() => setShowMmImport(true)}
|
||||
onExport={() => setShowExport(true)}
|
||||
onToggleReview={() => setReviewMode((v) => !v)}
|
||||
reviewMode={reviewMode}
|
||||
boardName={board?.name}
|
||||
/>}
|
||||
|
||||
@@ -643,6 +655,37 @@ export default function Editor({ isPublicView }: EditorProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Feedback / Review panel */}
|
||||
{reviewMode && annotationStore && user && resolvedBoardId && (
|
||||
<FeedbackPanel
|
||||
annotationStore={annotationStore}
|
||||
selectedObjectId={selectedLayerIds.length === 1 ? selectedLayerIds[0] : null}
|
||||
userId={user.id}
|
||||
boardId={resolvedBoardId}
|
||||
token={localStorage.getItem('token') || ''}
|
||||
canvasObjects={(() => {
|
||||
const scene = canvasRef.current?.getScene();
|
||||
const map = new Map<string, { id: string; name?: string; type: string }>();
|
||||
if (scene) {
|
||||
for (const item of scene.items.values()) {
|
||||
map.set(item.id, { id: item.id, name: item.data.name, type: item.data.type });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
})()}
|
||||
onJumpToObject={(objectId) => {
|
||||
const scene = canvasRef.current?.getScene();
|
||||
const vp = canvasRef.current?.getViewport();
|
||||
if (!scene || !vp) return;
|
||||
const item = scene.items.get(objectId);
|
||||
if (!item) return;
|
||||
const b = getItemWorldBounds(item);
|
||||
vp.animate({ time: 300, position: { x: b.x + b.w / 2, y: b.y + b.h / 2 }, ease: 'easeOutQuad' });
|
||||
selectionRef.current?.selectOnly(objectId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Upload progress panel */}
|
||||
<UploadPanel uploadManager={uploadManager} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user