Associate sticky notes with frames
This commit is contained in:
@@ -5,16 +5,19 @@ import { Rnd, RndDragCallback, RndResizeCallback } from 'react-rnd';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Button, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
import { useDispatch, useSelector } from 'app/types/store';
|
||||
|
||||
import {
|
||||
associatePostItWithFrame,
|
||||
bringPostItNoteToFront,
|
||||
disassociatePostItFromFrame,
|
||||
removePostItNote,
|
||||
updatePostItNoteColor,
|
||||
updatePostItNotePosition,
|
||||
updatePostItNoteSize,
|
||||
updatePostItNoteText,
|
||||
} from '../state/crdtSlice';
|
||||
import { selectFrames } from '../state/selectors';
|
||||
|
||||
interface ExploreMapStickyNoteProps {
|
||||
postIt: {
|
||||
@@ -23,6 +26,9 @@ interface ExploreMapStickyNoteProps {
|
||||
text: string;
|
||||
color: string;
|
||||
createdBy?: string;
|
||||
frameId?: string;
|
||||
frameOffsetX?: number;
|
||||
frameOffsetY?: number;
|
||||
};
|
||||
zoom: number;
|
||||
}
|
||||
@@ -44,8 +50,41 @@ export function ExploreMapStickyNote({ postIt, zoom }: ExploreMapStickyNoteProps
|
||||
const [editText, setEditText] = useState(postIt.text);
|
||||
const [dragStartPos, setDragStartPos] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
const frames = useSelector((state) => selectFrames(state.exploreMapCRDT));
|
||||
const activeFrameDragInfo = useSelector((state) => state.exploreMapCRDT.local.activeFrameDrag);
|
||||
|
||||
const colorInfo = STICKY_NOTE_COLORS.find((c) => c.name === postIt.color) || STICKY_NOTE_COLORS.find((c) => c.name === 'blue') || STICKY_NOTE_COLORS[0];
|
||||
|
||||
// Calculate effective position considering frame drag
|
||||
let effectiveX = postIt.position.x;
|
||||
let effectiveY = postIt.position.y;
|
||||
|
||||
// If this sticky note's frame is being dragged, apply the frame drag offset
|
||||
if (activeFrameDragInfo && postIt.frameId === activeFrameDragInfo.draggedFrameId) {
|
||||
effectiveX += activeFrameDragInfo.deltaX;
|
||||
effectiveY += activeFrameDragInfo.deltaY;
|
||||
}
|
||||
|
||||
// Check if sticky note intersects with a frame (>50% overlap)
|
||||
const checkFrameIntersection = useCallback(
|
||||
(x: number, y: number, width: number, height: number): string | null => {
|
||||
const postItArea = width * height;
|
||||
|
||||
for (const frame of Object.values(frames)) {
|
||||
const intersectX = Math.max(0, Math.min(x + width, frame.position.x + frame.position.width) - Math.max(x, frame.position.x));
|
||||
const intersectY = Math.max(0, Math.min(y + height, frame.position.y + frame.position.height) - Math.max(y, frame.position.y));
|
||||
const intersectArea = intersectX * intersectY;
|
||||
|
||||
if (intersectArea > postItArea * 0.5) {
|
||||
return frame.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[frames]
|
||||
);
|
||||
|
||||
const handleDragStart: RndDragCallback = useCallback(
|
||||
(_e, data) => {
|
||||
setDragStartPos({ x: data.x, y: data.y });
|
||||
@@ -71,11 +110,42 @@ export function ExploreMapStickyNote({ postIt, zoom }: ExploreMapStickyNoteProps
|
||||
y: data.y,
|
||||
})
|
||||
);
|
||||
|
||||
// Check if sticky note should be associated with a frame
|
||||
const intersectingFrameId = checkFrameIntersection(
|
||||
data.x,
|
||||
data.y,
|
||||
postIt.position.width,
|
||||
postIt.position.height
|
||||
);
|
||||
|
||||
if (intersectingFrameId && intersectingFrameId !== postIt.frameId) {
|
||||
// Sticky note moved into a frame
|
||||
const frame = frames[intersectingFrameId];
|
||||
const offsetX = data.x - frame.position.x;
|
||||
const offsetY = data.y - frame.position.y;
|
||||
|
||||
dispatch(
|
||||
associatePostItWithFrame({
|
||||
postItId: postIt.id,
|
||||
frameId: intersectingFrameId,
|
||||
offsetX,
|
||||
offsetY,
|
||||
})
|
||||
);
|
||||
} else if (!intersectingFrameId && postIt.frameId) {
|
||||
// Sticky note moved out of frame
|
||||
dispatch(
|
||||
disassociatePostItFromFrame({
|
||||
postItId: postIt.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
setDragStartPos(null);
|
||||
},
|
||||
[dispatch, postIt.id, dragStartPos]
|
||||
[dispatch, postIt.id, postIt.position.width, postIt.position.height, postIt.frameId, dragStartPos, checkFrameIntersection, frames]
|
||||
);
|
||||
|
||||
const handleResizeStop: RndResizeCallback = useCallback(
|
||||
@@ -100,8 +170,39 @@ export function ExploreMapStickyNote({ postIt, zoom }: ExploreMapStickyNoteProps
|
||||
height: newHeight,
|
||||
})
|
||||
);
|
||||
|
||||
// Check if sticky note should be associated with a frame after resize
|
||||
const intersectingFrameId = checkFrameIntersection(
|
||||
position.x,
|
||||
position.y,
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
|
||||
if (intersectingFrameId && intersectingFrameId !== postIt.frameId) {
|
||||
// Sticky note resized into a frame
|
||||
const frame = frames[intersectingFrameId];
|
||||
const offsetX = position.x - frame.position.x;
|
||||
const offsetY = position.y - frame.position.y;
|
||||
|
||||
dispatch(
|
||||
associatePostItWithFrame({
|
||||
postItId: postIt.id,
|
||||
frameId: intersectingFrameId,
|
||||
offsetX,
|
||||
offsetY,
|
||||
})
|
||||
);
|
||||
} else if (!intersectingFrameId && postIt.frameId) {
|
||||
// Sticky note resized out of frame
|
||||
dispatch(
|
||||
disassociatePostItFromFrame({
|
||||
postItId: postIt.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, postIt.id]
|
||||
[dispatch, postIt.id, postIt.frameId, checkFrameIntersection, frames]
|
||||
);
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
@@ -162,15 +263,19 @@ export function ExploreMapStickyNote({ postIt, zoom }: ExploreMapStickyNoteProps
|
||||
[dispatch, postIt.id]
|
||||
);
|
||||
|
||||
// Disable dragging when the frame is being dragged
|
||||
const isDraggingDisabled = !!(activeFrameDragInfo && postIt.frameId === activeFrameDragInfo.draggedFrameId);
|
||||
|
||||
return (
|
||||
<Rnd
|
||||
ref={rndRef}
|
||||
position={{ x: postIt.position.x, y: postIt.position.y }}
|
||||
position={{ x: effectiveX, y: effectiveY }}
|
||||
size={{ width: postIt.position.width, height: postIt.position.height }}
|
||||
scale={zoom}
|
||||
onDragStart={handleDragStart}
|
||||
onDragStop={handleDragStop}
|
||||
onResizeStop={handleResizeStop}
|
||||
disableDragging={isDraggingDisabled}
|
||||
bounds="parent"
|
||||
className={styles.postItContainer}
|
||||
style={{ zIndex: postIt.position.zIndex }}
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
UpdatePostItZIndexOperation,
|
||||
UpdatePostItTextOperation,
|
||||
UpdatePostItColorOperation,
|
||||
AssociatePostItWithFrameOperation,
|
||||
DisassociatePostItFromFrameOperation,
|
||||
OperationResult,
|
||||
CRDTExploreMapStateJSON,
|
||||
CommentData,
|
||||
@@ -237,6 +239,20 @@ export class CRDTStateManager {
|
||||
return panelIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get all sticky notes in a frame
|
||||
*/
|
||||
getPostItNotesInFrame(frameId: string): string[] {
|
||||
const postItIds: string[] = [];
|
||||
for (const postItId of this.getPostItNoteIds()) {
|
||||
const postIt = this.state.postItNoteData.get(postItId);
|
||||
if (postIt && postIt.frameId.get() === frameId) {
|
||||
postItIds.push(postItId);
|
||||
}
|
||||
}
|
||||
return postItIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an add panel operation
|
||||
*/
|
||||
@@ -872,6 +888,56 @@ export class CRDTStateManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an associate post-it note with frame operation
|
||||
*/
|
||||
createAssociatePostItWithFrameOperation(
|
||||
postItId: string,
|
||||
frameId: string,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
): AssociatePostItWithFrameOperation | null {
|
||||
if (!this.state.postItNotes.contains(postItId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
type: 'associate-postit-with-frame',
|
||||
mapUid: this.mapUid,
|
||||
operationId: uuidv4(),
|
||||
timestamp,
|
||||
nodeId: this.nodeId,
|
||||
payload: {
|
||||
postItId,
|
||||
frameId,
|
||||
offsetX,
|
||||
offsetY,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a disassociate post-it note from frame operation
|
||||
*/
|
||||
createDisassociatePostItFromFrameOperation(postItId: string): DisassociatePostItFromFrameOperation | null {
|
||||
if (!this.state.postItNotes.contains(postItId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamp = this.clock.tick();
|
||||
return {
|
||||
type: 'disassociate-postit-from-frame',
|
||||
mapUid: this.mapUid,
|
||||
operationId: uuidv4(),
|
||||
timestamp,
|
||||
nodeId: this.nodeId,
|
||||
payload: {
|
||||
postItId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all post-it note IDs
|
||||
*/
|
||||
@@ -910,6 +976,9 @@ export class CRDTStateManager {
|
||||
text: data.text.get(),
|
||||
color: data.color.get(),
|
||||
createdBy: data.createdBy.get(),
|
||||
frameId: data.frameId.get(),
|
||||
frameOffsetX: data.frameOffsetX.get(),
|
||||
frameOffsetY: data.frameOffsetY.get(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -922,6 +991,9 @@ export class CRDTStateManager {
|
||||
text: string;
|
||||
color: string;
|
||||
createdBy?: string;
|
||||
frameId?: string;
|
||||
frameOffsetX?: number;
|
||||
frameOffsetY?: number;
|
||||
}> {
|
||||
const postItNotes: Record<string, {
|
||||
id: string;
|
||||
@@ -929,6 +1001,9 @@ export class CRDTStateManager {
|
||||
text: string;
|
||||
color: string;
|
||||
createdBy?: string;
|
||||
frameId?: string;
|
||||
frameOffsetX?: number;
|
||||
frameOffsetY?: number;
|
||||
}> = {};
|
||||
for (const postItId of this.getPostItNoteIds()) {
|
||||
const postIt = this.getPostItNoteForUI(postItId);
|
||||
@@ -1000,6 +1075,10 @@ export class CRDTStateManager {
|
||||
return this.applyUpdatePostItText(operation);
|
||||
case 'update-postit-color':
|
||||
return this.applyUpdatePostItColor(operation);
|
||||
case 'associate-postit-with-frame':
|
||||
return this.applyAssociatePostItWithFrame(operation);
|
||||
case 'disassociate-postit-from-frame':
|
||||
return this.applyDisassociatePostItFromFrame(operation);
|
||||
case 'batch':
|
||||
return this.applyBatchOperation(operation);
|
||||
default: {
|
||||
@@ -1234,6 +1313,9 @@ export class CRDTStateManager {
|
||||
text: new LWWRegister(text || '', operation.timestamp),
|
||||
color: new LWWRegister(color || 'purple', operation.timestamp),
|
||||
createdBy: new LWWRegister(createdBy, operation.timestamp),
|
||||
frameId: new LWWRegister(undefined, operation.timestamp),
|
||||
frameOffsetX: new LWWRegister(undefined, operation.timestamp),
|
||||
frameOffsetY: new LWWRegister(undefined, operation.timestamp),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1344,6 +1426,45 @@ export class CRDTStateManager {
|
||||
};
|
||||
}
|
||||
|
||||
private applyAssociatePostItWithFrame(operation: AssociatePostItWithFrameOperation): OperationResult {
|
||||
const { postItId, frameId, offsetX, offsetY } = operation.payload;
|
||||
|
||||
const postItData = this.state.postItNoteData.get(postItId);
|
||||
|
||||
if (!postItData) {
|
||||
return { success: true, applied: false, error: 'Post-it note not found' };
|
||||
}
|
||||
|
||||
// Update frame association
|
||||
const frameIdUpdated = postItData.frameId.set(frameId, operation.timestamp);
|
||||
const offsetXUpdated = postItData.frameOffsetX.set(offsetX, operation.timestamp);
|
||||
const offsetYUpdated = postItData.frameOffsetY.set(offsetY, operation.timestamp);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
applied: frameIdUpdated || offsetXUpdated || offsetYUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
private applyDisassociatePostItFromFrame(operation: DisassociatePostItFromFrameOperation): OperationResult {
|
||||
const { postItId } = operation.payload;
|
||||
const postItData = this.state.postItNoteData.get(postItId);
|
||||
|
||||
if (!postItData) {
|
||||
return { success: true, applied: false, error: 'Post-it note not found' };
|
||||
}
|
||||
|
||||
// Clear frame association
|
||||
const frameIdUpdated = postItData.frameId.set(undefined, operation.timestamp);
|
||||
const offsetXUpdated = postItData.frameOffsetX.set(undefined, operation.timestamp);
|
||||
const offsetYUpdated = postItData.frameOffsetY.set(undefined, operation.timestamp);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
applied: frameIdUpdated || offsetXUpdated || offsetYUpdated,
|
||||
};
|
||||
}
|
||||
|
||||
private applyBatchOperation(operation: BatchOperation): OperationResult {
|
||||
let anyApplied = false;
|
||||
const errors: string[] = [];
|
||||
@@ -1553,6 +1674,9 @@ export class CRDTStateManager {
|
||||
text: otherPostItData.text.clone(),
|
||||
color: otherPostItData.color.clone(),
|
||||
createdBy: otherPostItData.createdBy.clone(),
|
||||
frameId: otherPostItData.frameId.clone(),
|
||||
frameOffsetX: otherPostItData.frameOffsetX.clone(),
|
||||
frameOffsetY: otherPostItData.frameOffsetY.clone(),
|
||||
});
|
||||
} else {
|
||||
// Merge each LWW register
|
||||
@@ -1564,6 +1688,9 @@ export class CRDTStateManager {
|
||||
myPostItData.text.merge(otherPostItData.text);
|
||||
myPostItData.color.merge(otherPostItData.color);
|
||||
myPostItData.createdBy.merge(otherPostItData.createdBy);
|
||||
myPostItData.frameId.merge(otherPostItData.frameId);
|
||||
myPostItData.frameOffsetX.merge(otherPostItData.frameOffsetX);
|
||||
myPostItData.frameOffsetY.merge(otherPostItData.frameOffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1717,6 +1844,9 @@ export class CRDTStateManager {
|
||||
text: data.text.toJSON(),
|
||||
color: data.color.toJSON(),
|
||||
createdBy: data.createdBy.toJSON(),
|
||||
frameId: data.frameId.toJSON(),
|
||||
frameOffsetX: data.frameOffsetX.toJSON(),
|
||||
frameOffsetY: data.frameOffsetY.toJSON(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1766,6 +1896,9 @@ export class CRDTStateManager {
|
||||
text: LWWRegister.fromJSON(data.text),
|
||||
color: LWWRegister.fromJSON(data.color),
|
||||
createdBy: data.createdBy ? LWWRegister.fromJSON(data.createdBy) : new LWWRegister(undefined, defaultTimestamp),
|
||||
frameId: (data as any).frameId ? LWWRegister.fromJSON((data as any).frameId) : new LWWRegister(undefined, defaultTimestamp),
|
||||
frameOffsetX: (data as any).frameOffsetX ? LWWRegister.fromJSON((data as any).frameOffsetX) : new LWWRegister(undefined, defaultTimestamp),
|
||||
frameOffsetY: (data as any).frameOffsetY ? LWWRegister.fromJSON((data as any).frameOffsetY) : new LWWRegister(undefined, defaultTimestamp),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,11 @@ export interface CRDTPostItNoteData {
|
||||
|
||||
// Creator metadata
|
||||
createdBy: LWWRegister<string | undefined>;
|
||||
|
||||
// Frame association properties
|
||||
frameId: LWWRegister<string | undefined>;
|
||||
frameOffsetX: LWWRegister<number | undefined>;
|
||||
frameOffsetY: LWWRegister<number | undefined>;
|
||||
}
|
||||
|
||||
export interface CRDTExploreMapState {
|
||||
@@ -182,6 +187,9 @@ export interface CRDTExploreMapStateJSON {
|
||||
text: { value: string; timestamp: HLCTimestamp };
|
||||
color: { value: string; timestamp: HLCTimestamp };
|
||||
createdBy?: { value: string | undefined; timestamp: HLCTimestamp };
|
||||
frameId?: { value: string | undefined; timestamp: HLCTimestamp };
|
||||
frameOffsetX?: { value: number | undefined; timestamp: HLCTimestamp };
|
||||
frameOffsetY?: { value: number | undefined; timestamp: HLCTimestamp };
|
||||
}>;
|
||||
panels: {
|
||||
adds: Record<string, string[]>;
|
||||
@@ -257,6 +265,8 @@ export type CRDTOperationType =
|
||||
| 'update-postit-zindex'
|
||||
| 'update-postit-text'
|
||||
| 'update-postit-color'
|
||||
| 'associate-postit-with-frame'
|
||||
| 'disassociate-postit-from-frame'
|
||||
| 'batch'; // For batching multiple operations
|
||||
|
||||
/**
|
||||
@@ -592,6 +602,29 @@ export interface UpdatePostItColorOperation extends CRDTOperationBase {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate post-it note with frame operation
|
||||
*/
|
||||
export interface AssociatePostItWithFrameOperation extends CRDTOperationBase {
|
||||
type: 'associate-postit-with-frame';
|
||||
payload: {
|
||||
postItId: string;
|
||||
frameId: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disassociate post-it note from frame operation
|
||||
*/
|
||||
export interface DisassociatePostItFromFrameOperation extends CRDTOperationBase {
|
||||
type: 'disassociate-postit-from-frame';
|
||||
payload: {
|
||||
postItId: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Batch operation (multiple operations in one)
|
||||
@@ -633,6 +666,8 @@ export type CRDTOperation =
|
||||
| UpdatePostItZIndexOperation
|
||||
| UpdatePostItTextOperation
|
||||
| UpdatePostItColorOperation
|
||||
| AssociatePostItWithFrameOperation
|
||||
| DisassociatePostItFromFrameOperation
|
||||
| BatchOperation;
|
||||
|
||||
/**
|
||||
|
||||
@@ -715,6 +715,55 @@ const crdtSlice = createSlice({
|
||||
state.pendingOperations.push(operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Associate post-it note with frame
|
||||
*/
|
||||
associatePostItWithFrame: (state, action: PayloadAction<{
|
||||
postItId: string;
|
||||
frameId: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}>) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
const operation = manager.createAssociatePostItWithFrameOperation(
|
||||
action.payload.postItId,
|
||||
action.payload.frameId,
|
||||
action.payload.offsetX,
|
||||
action.payload.offsetY
|
||||
);
|
||||
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.applyOperation(operation);
|
||||
saveCRDTManager(state, manager);
|
||||
state.pendingOperations.push(operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Disassociate post-it note from frame
|
||||
*/
|
||||
disassociatePostItFromFrame: (state, action: PayloadAction<{
|
||||
postItId: string;
|
||||
}>) => {
|
||||
const manager = getCRDTManager(state);
|
||||
|
||||
const operation = manager.createDisassociatePostItFromFrameOperation(
|
||||
action.payload.postItId
|
||||
);
|
||||
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
|
||||
manager.applyOperation(operation);
|
||||
saveCRDTManager(state, manager);
|
||||
|
||||
state.pendingOperations.push(operation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Duplicate a panel
|
||||
*/
|
||||
@@ -878,16 +927,36 @@ const crdtSlice = createSlice({
|
||||
}
|
||||
}
|
||||
|
||||
// Create operations for all child sticky notes
|
||||
const postItOps: CRDTOperation[] = [];
|
||||
const childPostItIds = manager.getPostItNotesInFrame(action.payload.frameId);
|
||||
|
||||
for (const postItId of childPostItIds) {
|
||||
const postIt = manager.getPostItNoteData(postItId);
|
||||
if (postIt) {
|
||||
const newX = postIt.positionX.get() + deltaX;
|
||||
const newY = postIt.positionY.get() + deltaY;
|
||||
|
||||
const postItOp = manager.createUpdatePostItPositionOperation(postItId, newX, newY);
|
||||
if (postItOp) {
|
||||
postItOps.push(postItOp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all operations
|
||||
manager.applyOperation(frameOp);
|
||||
for (const op of panelOps) {
|
||||
manager.applyOperation(op);
|
||||
}
|
||||
for (const op of postItOps) {
|
||||
manager.applyOperation(op);
|
||||
}
|
||||
|
||||
saveCRDTManager(state, manager);
|
||||
|
||||
// Push all operations for broadcast
|
||||
state.pendingOperations.push(frameOp, ...panelOps);
|
||||
state.pendingOperations.push(frameOp, ...panelOps, ...postItOps);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1294,6 +1363,8 @@ export const {
|
||||
bringPostItNoteToFront,
|
||||
updatePostItNoteText,
|
||||
updatePostItNoteColor,
|
||||
associatePostItWithFrame,
|
||||
disassociatePostItFromFrame,
|
||||
duplicatePanel,
|
||||
addFrame,
|
||||
removeFrame,
|
||||
|
||||
@@ -36,6 +36,12 @@ const panelCache = new Map<string, { panel: ExploreMapPanel; data: string }>();
|
||||
*/
|
||||
const frameCache = new Map<string, { frame: ExploreMapFrame; data: string }>();
|
||||
|
||||
/**
|
||||
* Cache for individual post-it note objects
|
||||
* Key: postItId, Value: { postIt object, serialized postIt data for comparison }
|
||||
*/
|
||||
const postItCache = new Map<string, { postIt: any; data: string }>();
|
||||
|
||||
/**
|
||||
* Select all panels as a Record (for compatibility with existing UI)
|
||||
*
|
||||
@@ -259,12 +265,66 @@ export const selectComments = createSelector(
|
||||
|
||||
/**
|
||||
* Select all post-it notes as a Record
|
||||
*
|
||||
* OPTIMIZATION: This selector caches individual post-it note objects and only creates
|
||||
* new post-it note objects when the underlying CRDT data for that specific post-it note changes.
|
||||
* This prevents unnecessary re-renders of unchanged post-it notes when another post-it note is modified.
|
||||
*/
|
||||
export const selectPostItNotes = createSelector(
|
||||
[(state: ExploreMapCRDTState) => state],
|
||||
(state) => {
|
||||
[
|
||||
(state: ExploreMapCRDTState) => state.crdtStateJSON,
|
||||
(state: ExploreMapCRDTState) => state.nodeId,
|
||||
(state: ExploreMapCRDTState) => state.uid,
|
||||
],
|
||||
(crdtStateJSON, nodeId, uid): Record<string, any> => {
|
||||
const state: ExploreMapCRDTState = {
|
||||
uid,
|
||||
crdtStateJSON,
|
||||
nodeId,
|
||||
sessionId: '', // Not needed for post-it note selection
|
||||
pendingOperations: [],
|
||||
local: {
|
||||
viewport: { zoom: 1, panX: 0, panY: 0 },
|
||||
selectedPanelIds: [],
|
||||
cursors: {},
|
||||
cursorMode: 'pointer',
|
||||
isOnline: false,
|
||||
isSyncing: false,
|
||||
},
|
||||
};
|
||||
const manager = getCRDTManager(state);
|
||||
return manager.getAllPostItNotesForUI();
|
||||
const postItNotes: Record<string, any> = {};
|
||||
const currentPostItIds = new Set(manager.getPostItNoteIds());
|
||||
|
||||
// Clean up cache for removed post-it notes
|
||||
for (const cachedPostItId of postItCache.keys()) {
|
||||
if (!currentPostItIds.has(cachedPostItId)) {
|
||||
postItCache.delete(cachedPostItId);
|
||||
}
|
||||
}
|
||||
|
||||
// Build post-it notes object, reusing cached objects when data hasn't changed
|
||||
for (const postItId of currentPostItIds) {
|
||||
const postItData = manager.getPostItNoteForUI(postItId);
|
||||
if (!postItData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Serialize the post-it note data to detect changes
|
||||
const serializedData = JSON.stringify(postItData);
|
||||
const cached = postItCache.get(postItId);
|
||||
|
||||
// Reuse cached post-it note if data hasn't changed
|
||||
if (cached && cached.data === serializedData) {
|
||||
postItNotes[postItId] = cached.postIt;
|
||||
} else {
|
||||
// Post-it note is new or changed, create new object and cache it
|
||||
postItNotes[postItId] = postItData;
|
||||
postItCache.set(postItId, { postIt: postItData, data: serializedData });
|
||||
}
|
||||
}
|
||||
|
||||
return postItNotes;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user