-
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
useQuery,
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useHistory } from "../hooks/useHistory";
|
||||
import { useCollaborationRoom } from "../contexts/CollaborationContext";
|
||||
import type {
|
||||
DataChangeMessage,
|
||||
ItemCreatedMessage,
|
||||
ItemDeletedMessage,
|
||||
} from "../services/collaboration";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
@@ -172,6 +178,16 @@ export default function ReportEditorPage() {
|
||||
// Auto-save feature - enabled by default
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
||||
|
||||
// ============ COLLABORATION (using global context) ============
|
||||
// Room key format: "report-template:{id}"
|
||||
const roomKey = id ? `report-template:${id}` : null;
|
||||
const collaboration = useCollaborationRoom(roomKey, {
|
||||
enabled: !isNew && !!id,
|
||||
});
|
||||
|
||||
// Flag to prevent re-broadcasting received changes
|
||||
const isApplyingRemoteChange = useRef(false);
|
||||
|
||||
// Update zoom on screen size change
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
@@ -183,6 +199,171 @@ export default function ReportEditorPage() {
|
||||
}
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
// ============ COLLABORATION EFFECTS ============
|
||||
// The collaboration context handles connection, room joining, and presence automatically.
|
||||
// We only need to subscribe to data change events and send our changes.
|
||||
|
||||
// Subscribe to remote data changes
|
||||
useEffect(() => {
|
||||
if (!collaboration.isConnected || !collaboration.currentRoom) return;
|
||||
|
||||
const unsubscribers: (() => void)[] = [];
|
||||
|
||||
// Element/data changed by remote user
|
||||
unsubscribers.push(
|
||||
collaboration.onDataChanged((message: DataChangeMessage) => {
|
||||
if (message.itemType !== "element") return;
|
||||
isApplyingRemoteChange.current = true;
|
||||
historyActions.setWithoutHistory((prev) => ({
|
||||
...prev,
|
||||
elements: prev.elements.map((el) =>
|
||||
el.id === message.itemId
|
||||
? { ...el, ...(message.newValue as Partial<AprtElement>) }
|
||||
: el,
|
||||
),
|
||||
}));
|
||||
setTimeout(() => {
|
||||
isApplyingRemoteChange.current = false;
|
||||
}, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// Element added by remote user
|
||||
unsubscribers.push(
|
||||
collaboration.onItemCreated((message: ItemCreatedMessage) => {
|
||||
if (message.itemType !== "element") return;
|
||||
isApplyingRemoteChange.current = true;
|
||||
historyActions.setWithoutHistory((prev) => ({
|
||||
...prev,
|
||||
elements: [...prev.elements, message.item as AprtElement],
|
||||
}));
|
||||
setTimeout(() => {
|
||||
isApplyingRemoteChange.current = false;
|
||||
}, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// Element deleted by remote user
|
||||
unsubscribers.push(
|
||||
collaboration.onItemDeleted((message: ItemDeletedMessage) => {
|
||||
if (message.itemType !== "element") return;
|
||||
isApplyingRemoteChange.current = true;
|
||||
historyActions.setWithoutHistory((prev) => ({
|
||||
...prev,
|
||||
elements: prev.elements.filter((el) => el.id !== message.itemId),
|
||||
}));
|
||||
// Clear selection if deleted element was selected
|
||||
if (selectedElementId === message.itemId) {
|
||||
setSelectedElementId(null);
|
||||
}
|
||||
setTimeout(() => {
|
||||
isApplyingRemoteChange.current = false;
|
||||
}, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// Page changes by remote user
|
||||
unsubscribers.push(
|
||||
collaboration.onDataChanged((message: DataChangeMessage) => {
|
||||
if (message.itemType !== "page") return;
|
||||
isApplyingRemoteChange.current = true;
|
||||
historyActions.setWithoutHistory((prev) => {
|
||||
switch (message.changeType) {
|
||||
case "added":
|
||||
return {
|
||||
...prev,
|
||||
pages: [...prev.pages, message.newValue as AprtPage],
|
||||
};
|
||||
case "deleted":
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.filter((p) => p.id !== message.itemId),
|
||||
elements: prev.elements.filter(
|
||||
(e) => e.pageId !== message.itemId,
|
||||
),
|
||||
};
|
||||
case "renamed":
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map((p) =>
|
||||
p.id === message.itemId
|
||||
? { ...p, name: message.newValue as string }
|
||||
: p,
|
||||
),
|
||||
};
|
||||
case "reordered":
|
||||
return {
|
||||
...prev,
|
||||
pages: message.newValue as AprtPage[],
|
||||
};
|
||||
case "settings":
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map((p) =>
|
||||
p.id === message.itemId
|
||||
? { ...p, ...(message.newValue as Partial<AprtPage>) }
|
||||
: p,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
isApplyingRemoteChange.current = false;
|
||||
}, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// Template saved by remote user
|
||||
unsubscribers.push(
|
||||
collaboration.onDataSaved((message) => {
|
||||
console.log(
|
||||
"[Collaboration] Received DataSaved from:",
|
||||
message.savedBy,
|
||||
);
|
||||
setSnackbar({
|
||||
open: true,
|
||||
message: `${message.savedBy} ha salvato il template`,
|
||||
severity: "success",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["report-template", id] });
|
||||
}),
|
||||
);
|
||||
|
||||
// Sync requested - send current template to requester
|
||||
unsubscribers.push(
|
||||
collaboration.onSyncRequested((request) => {
|
||||
collaboration.sendSync(request.requesterId, JSON.stringify(template));
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribers.forEach((unsub) => unsub());
|
||||
};
|
||||
}, [
|
||||
collaboration,
|
||||
historyActions,
|
||||
selectedElementId,
|
||||
template,
|
||||
queryClient,
|
||||
id,
|
||||
]);
|
||||
|
||||
// Send selection changes to collaborators
|
||||
useEffect(() => {
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendSelectionChanged(selectedElementId);
|
||||
}
|
||||
}, [collaboration, selectedElementId]);
|
||||
|
||||
// Send view/page navigation to collaborators
|
||||
useEffect(() => {
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendViewChanged(currentPageId);
|
||||
}
|
||||
}, [collaboration, currentPageId]);
|
||||
|
||||
// Load existing template
|
||||
const { data: existingTemplate, isLoading: isLoadingTemplate } = useQuery({
|
||||
queryKey: ["report-template", id],
|
||||
@@ -347,6 +528,19 @@ export default function ReportEditorPage() {
|
||||
setSaveDialog(false);
|
||||
// Mark current state as saved
|
||||
setLastSavedUndoCount(templateHistory.undoCount);
|
||||
|
||||
// Notify collaborators of save
|
||||
console.log(
|
||||
"[AutoSave] Save success, collaboration.isConnected:",
|
||||
collaboration.isConnected,
|
||||
"currentRoom:",
|
||||
collaboration.currentRoom,
|
||||
);
|
||||
if (collaboration.isConnected && collaboration.currentRoom) {
|
||||
console.log("[AutoSave] Sending DataSaved notification");
|
||||
collaboration.sendDataSaved();
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
navigate(`/report-editor/${result.id}`, { replace: true });
|
||||
}
|
||||
@@ -415,10 +609,15 @@ export default function ReportEditorPage() {
|
||||
pages: [...prev.pages, newPage],
|
||||
}));
|
||||
|
||||
// Send to collaborators
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendDataChanged(newPageId, "page", "added", newPage);
|
||||
}
|
||||
|
||||
// Switch to the new page
|
||||
setCurrentPageId(newPageId);
|
||||
setSelectedElementId(null);
|
||||
}, [template.pages.length, historyActions]);
|
||||
}, [template.pages.length, historyActions, collaboration]);
|
||||
|
||||
// Duplicate page with all its elements
|
||||
const handleDuplicatePage = useCallback(
|
||||
@@ -465,6 +664,11 @@ export default function ReportEditorPage() {
|
||||
|
||||
const pageIndex = template.pages.findIndex((p) => p.id === pageId);
|
||||
|
||||
// Send to collaborators before deleting
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendDataChanged(pageId, "page", "deleted", null);
|
||||
}
|
||||
|
||||
historyActions.set((prev) => ({
|
||||
...prev,
|
||||
pages: prev.pages.filter((p) => p.id !== pageId),
|
||||
@@ -481,7 +685,7 @@ export default function ReportEditorPage() {
|
||||
}
|
||||
setSelectedElementId(null);
|
||||
},
|
||||
[template.pages, historyActions],
|
||||
[template.pages, historyActions, collaboration],
|
||||
);
|
||||
|
||||
// Rename page
|
||||
@@ -493,8 +697,13 @@ export default function ReportEditorPage() {
|
||||
p.id === pageId ? { ...p, name: newName } : p,
|
||||
),
|
||||
}));
|
||||
|
||||
// Send to collaborators
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendDataChanged(pageId, "page", "renamed", newName);
|
||||
}
|
||||
},
|
||||
[historyActions],
|
||||
[historyActions, collaboration],
|
||||
);
|
||||
|
||||
// Move page up or down
|
||||
@@ -578,12 +787,17 @@ export default function ReportEditorPage() {
|
||||
}));
|
||||
setSelectedElementId(newElement.id);
|
||||
|
||||
// Send to collaborators
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendItemCreated(newElement.id, "element", newElement);
|
||||
}
|
||||
|
||||
// On mobile, open properties panel after adding element
|
||||
if (isMobile) {
|
||||
setMobilePanel("properties");
|
||||
}
|
||||
},
|
||||
[historyActions, currentPageId, isMobile],
|
||||
[historyActions, currentPageId, isMobile, collaboration],
|
||||
);
|
||||
|
||||
// Update element without history (for continuous updates like dragging)
|
||||
@@ -608,8 +822,13 @@ export default function ReportEditorPage() {
|
||||
el.id === elementId ? { ...el, ...updates } : el,
|
||||
),
|
||||
}));
|
||||
|
||||
// Send to collaborators
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendDataChanged(elementId, "element", "full", updates);
|
||||
}
|
||||
},
|
||||
[historyActions],
|
||||
[historyActions, collaboration],
|
||||
);
|
||||
|
||||
// Handle image selection from dialog
|
||||
@@ -696,12 +915,18 @@ export default function ReportEditorPage() {
|
||||
// Delete element
|
||||
const handleDeleteElement = useCallback(() => {
|
||||
if (!selectedElementId) return;
|
||||
|
||||
// Send to collaborators before deleting
|
||||
if (collaboration.isConnected && !isApplyingRemoteChange.current) {
|
||||
collaboration.sendItemDeleted(selectedElementId, "element");
|
||||
}
|
||||
|
||||
historyActions.set((prev) => ({
|
||||
...prev,
|
||||
elements: prev.elements.filter((el) => el.id !== selectedElementId),
|
||||
}));
|
||||
setSelectedElementId(null);
|
||||
}, [selectedElementId, historyActions]);
|
||||
}, [selectedElementId, historyActions, collaboration]);
|
||||
|
||||
// Copy element
|
||||
const handleCopyElement = useCallback(() => {
|
||||
@@ -1228,29 +1453,37 @@ export default function ReportEditorPage() {
|
||||
]);
|
||||
|
||||
// Auto-save effect - saves after 1 second of inactivity when there are unsaved changes
|
||||
// Use refs to avoid the effect re-running on every render due to saveMutation changing
|
||||
const saveMutationRef = useRef(saveMutation);
|
||||
saveMutationRef.current = saveMutation;
|
||||
|
||||
const templateRef = useRef(template);
|
||||
templateRef.current = template;
|
||||
|
||||
const templateInfoRef = useRef(templateInfo);
|
||||
templateInfoRef.current = templateInfo;
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!autoSaveEnabled ||
|
||||
!hasUnsavedChanges ||
|
||||
isNew ||
|
||||
saveMutation.isPending
|
||||
saveMutationRef.current.isPending
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
saveMutation.mutate({ template, info: templateInfo });
|
||||
if (!saveMutationRef.current.isPending) {
|
||||
saveMutationRef.current.mutate({
|
||||
template: templateRef.current,
|
||||
info: templateInfoRef.current,
|
||||
});
|
||||
}
|
||||
}, 1000); // 1 second debounce
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [
|
||||
autoSaveEnabled,
|
||||
hasUnsavedChanges,
|
||||
isNew,
|
||||
template,
|
||||
templateInfo,
|
||||
saveMutation,
|
||||
]);
|
||||
}, [autoSaveEnabled, hasUnsavedChanges, isNew]);
|
||||
|
||||
if (isLoadingTemplate && id) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user