This commit is contained in:
2025-11-29 00:50:22 +01:00
parent 6bc65c2a29
commit 30b4bb4626
11 changed files with 518 additions and 59 deletions

View File

@@ -178,6 +178,9 @@ export default function ReportEditorPage() {
// Auto-save feature - enabled by default
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
// Template version for collaboration sync (increments on each save)
const templateVersionRef = useRef(0);
// ============ COLLABORATION (using global context) ============
// Room key format: "report-template:{id}"
const roomKey = id ? `report-template:${id}` : null;
@@ -315,19 +318,68 @@ export default function ReportEditorPage() {
}),
);
// Template saved by remote user
// Template sync received - apply template directly without server reload
unsubscribers.push(
collaboration.onTemplateSync((message) => {
console.log(
`[Collaboration] Received TemplateSync, version ${message.version}, size: ${message.templateJson.length} bytes`,
);
// Only apply if the received version is newer
if (message.version <= templateVersionRef.current) {
console.log(
`[Collaboration] Ignoring older template version ${message.version} (current: ${templateVersionRef.current})`,
);
return;
}
try {
isApplyingRemoteChange.current = true;
const receivedTemplate = JSON.parse(
message.templateJson,
) as AprtTemplate;
// Update version to received version
templateVersionRef.current = message.version;
// Apply template without adding to history (it's a sync, not a user action)
historyActions.setWithoutHistory(() => receivedTemplate);
// Show notification
setSnackbar({
open: true,
message: "Template aggiornato da un altro utente",
severity: "success",
});
// Also update the query cache so it stays in sync
queryClient.setQueryData(["report-template", id], (old: unknown) => {
if (!old) return old;
return {
...(old as Record<string, unknown>),
templateJson: message.templateJson,
};
});
} catch (e) {
console.error("[Collaboration] Error parsing received template:", e);
} finally {
setTimeout(() => {
isApplyingRemoteChange.current = false;
}, 0);
}
}),
);
// Legacy DataSaved handler (for backwards compatibility - will be removed)
unsubscribers.push(
collaboration.onDataSaved((message) => {
console.log(
"[Collaboration] Received DataSaved from:",
message.savedBy,
"(legacy - should use TemplateSync instead)",
);
setSnackbar({
open: true,
message: `${message.savedBy} ha salvato il template`,
severity: "success",
});
queryClient.invalidateQueries({ queryKey: ["report-template", id] });
// Only reload from server if we haven't received a TemplateSync
// This is a fallback for older clients
}),
);
@@ -529,16 +581,30 @@ export default function ReportEditorPage() {
// Mark current state as saved
setLastSavedUndoCount(templateHistory.undoCount);
// Notify collaborators of save
console.log(
"[AutoSave] Save success, collaboration.isConnected:",
collaboration.isConnected,
"currentRoom:",
collaboration.currentRoom,
);
// Broadcast template to collaborators (instant sync without server reload)
if (collaboration.isConnected && collaboration.currentRoom) {
console.log("[AutoSave] Sending DataSaved notification");
collaboration.sendDataSaved();
// Update dataSources in template for broadcasting
const updatedTemplateForSync = {
...template,
dataSources: selectedDatasets.reduce(
(acc, ds) => {
acc[ds.id] = { type: "object" as const, schema: ds.id };
return acc;
},
{} as Record<string, { type: "object"; schema: string }>,
),
};
// Increment version and broadcast
templateVersionRef.current += 1;
const templateJson = JSON.stringify(updatedTemplateForSync);
console.log(
`[AutoSave] Broadcasting template sync, version ${templateVersionRef.current}, size: ${templateJson.length} bytes`,
);
collaboration.broadcastTemplateSync(
templateJson,
templateVersionRef.current,
);
}
if (isNew) {
@@ -1452,38 +1518,49 @@ export default function ReportEditorPage() {
selectedElementId,
]);
// 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
// Auto-save: simple debounced save on every template change
const autoSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const saveMutationRef = useRef(saveMutation);
saveMutationRef.current = saveMutation;
const templateRef = useRef(template);
templateRef.current = template;
const templateInfoRef = useRef(templateInfo);
templateInfoRef.current = templateInfo;
const templateForSaveRef = useRef(template);
templateForSaveRef.current = template;
const templateInfoForSaveRef = useRef(templateInfo);
templateInfoForSaveRef.current = templateInfo;
useEffect(() => {
if (
!autoSaveEnabled ||
!hasUnsavedChanges ||
isNew ||
saveMutationRef.current.isPending
) {
// Skip if disabled or new template
if (!autoSaveEnabled || isNew) {
return;
}
const timeoutId = setTimeout(() => {
// Skip if no unsaved changes
if (!hasUnsavedChanges) {
return;
}
// Clear previous timeout (debounce)
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
// Save after 500ms debounce
autoSaveTimeoutRef.current = setTimeout(() => {
// Check if not already saving at the moment of execution
if (!saveMutationRef.current.isPending) {
console.log("[AutoSave] Saving...");
saveMutationRef.current.mutate({
template: templateRef.current,
info: templateInfoRef.current,
template: templateForSaveRef.current,
info: templateInfoForSaveRef.current,
});
}
}, 1000); // 1 second debounce
}, 500);
return () => clearTimeout(timeoutId);
}, [autoSaveEnabled, hasUnsavedChanges, isNew]);
return () => {
if (autoSaveTimeoutRef.current) {
clearTimeout(autoSaveTimeoutRef.current);
}
};
}, [autoSaveEnabled, isNew, hasUnsavedChanges, templateHistory.undoCount]);
if (isLoadingTemplate && id) {
return (