• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alkem-io / client-web / #9048

11 Oct 2024 01:42PM UTC coverage: 5.943%. First build
#9048

Pull #7022

travis-ci

Pull Request #7022: [v0.74.0] Roles API + Unauthenticated Explore page

202 of 10241 branches covered (1.97%)

Branch coverage included in aggregate %.

63 of 431 new or added lines in 60 files covered. (14.62%)

1468 of 17861 relevant lines covered (8.22%)

0.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/domain/collaboration/whiteboard/utils/mergeWhiteboard.ts
1
import type { ExcalidrawElement } from '@alkemio/excalidraw/dist/types/element/src/types';
2
import type { BinaryFileData, ExcalidrawImperativeAPI } from '@alkemio/excalidraw/dist/types/excalidraw/types';
3
import { v4 as uuidv4 } from 'uuid';
4
import type {
5
  CaptureUpdateAction as ExcalidrawCaptureUpdateAction,
6
  hashElementsVersion as ExcalidrawHashElementsVersion,
7
} from '@alkemio/excalidraw';
8
import { lazyImportWithErrorHandler } from '@/core/lazyLoading/lazyWithGlobalErrorHandler';
9

10
const ANIMATION_SPEED = 2000;
11
const ANIMATION_ZOOM_FACTOR = 0.75;
12

13
type ExcalidrawElementWithContainerId = ExcalidrawElement & { containerId: string | null };
14
type ExcalidrawUtils = {
15
  CaptureUpdateAction: typeof ExcalidrawCaptureUpdateAction;
16
  hashElementsVersion: typeof ExcalidrawHashElementsVersion;
×
17
};
×
18

×
19
class WhiteboardMergeError extends Error {}
20

21
interface WhiteboardLike {
×
22
  type: string;
×
23
  version: number;
×
24
  elements: ExcalidrawElement[];
25
  files?: Record<BinaryFileData['id'], BinaryFileData>;
×
26
}
×
27

28
const isWhiteboardLike = (parsedObject: unknown): parsedObject is WhiteboardLike => {
29
  if (!parsedObject) {
×
30
    return false;
31
  }
32

33
  const whiteboard = parsedObject as Record<string, unknown>;
34
  if (whiteboard['type'] !== 'excalidraw' || whiteboard['version'] !== 2) {
35
    return false;
36
  }
37
  if (!whiteboard['elements'] || !Array.isArray(whiteboard['elements'])) {
38
    return false;
39
  }
×
40
  // At least we have something that looks like a whiteboard
×
41
  return true;
×
42
};
43

44
interface BoundingBox {
×
45
  minX: number;
46
  minY: number;
×
47
  maxX: number;
48
  maxY: number;
49
}
50

51
const getBoundingBox = (whiteboardElements?: readonly ExcalidrawElement[]): BoundingBox => {
52
  if (!whiteboardElements || whiteboardElements.length === 0) {
53
    return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
×
54
  }
×
55

×
56
  const [firstElement, ...elements] = whiteboardElements;
×
57

×
58
  const box = {
59
    minX: firstElement.x,
×
60
    minY: firstElement.y,
61
    maxX: firstElement.x + firstElement.width,
62
    maxY: firstElement.y + firstElement.height,
×
63
  };
64

65
  elements.forEach(element => {
×
66
    box.minX = Math.min(element.x, box.minX);
×
67
    box.minY = Math.min(element.y, box.minY);
68
    box.maxX = Math.max(element.x + element.width, box.maxX);
×
69
    box.maxY = Math.max(element.y + element.height, box.maxY);
70
  });
71
  return box;
×
72
};
73

×
74
const calculateInsertionPoint = (whiteboardA: BoundingBox, whiteboardB: BoundingBox): { x: number; y: number } => {
75
  // Center the whiteboardB vertically in reference to whiteboardA
76
  // minY - height / 2
77
  const aY = whiteboardA.minY + (whiteboardA.maxY - whiteboardA.minY) / 2;
78
  const bY = whiteboardB.minY + (whiteboardB.maxY - whiteboardB.minY) / 2;
79
  // Displace middle of whiteboardB to middle of whiteboardA
80
  const y = aY - bY;
81

82
  // Displace all elements of whiteboardB to the right of whiteboardA + 10% of the width of whiteboardA
×
83
  const x = -whiteboardB.minX + whiteboardA.maxX + 0.1 * (whiteboardA.maxX - whiteboardA.minX);
84

85
  return { x, y };
86
};
87

88
/**
89
 * Generate new element ids and store them in the idsMap.
90
 * This is done to avoid id collisions when inserting multiple times the same template into a whiteboard.
×
91
 * @param idsMap
92
 * @returns a function that can be passed to elements.map
93
 */
94
const generateNewIds = (idsMap: Record<string, string>) => (element: ExcalidrawElement) => ({
95
  ...element,
96
  id: (idsMap[element.id] = uuidv4()), // Replace the id and store it in the map
97
});
98

NEW
99
/**
×
100
 * Returns a function that can be passed to elements.map to replace the version of the elements
×
101
 */
×
102
const replaceElementVersion = (version: number) => (element: ExcalidrawElement) => ({
×
103
  ...element,
104
  version,
×
105
});
×
106

107
/**
108
 * Returns a function that can be passed to elements.map to replace containerId and boundElements ids
109
 */
110
const replaceBoundElementsIds = (idsMap: Record<string, string>) => {
×
111
  const replace = (id: string | null) => (id ? idsMap[id] || id : id);
×
112
  const replaceMultiple = (boundElements: ExcalidrawElement['boundElements']) =>
113
    boundElements
114
      ? boundElements.map(boundElement => ({ ...boundElement, id: idsMap[boundElement.id] || boundElement.id }))
115
      : boundElements;
116

×
117
  return (element: ExcalidrawElement) => ({
118
    ...element,
119
    containerId: replace((element as ExcalidrawElementWithContainerId).containerId),
120
    boundElements: replaceMultiple(element.boundElements),
121
  });
122
};
123

NEW
124
/**
×
125
 * Returns a function that can be passed to elements.map, to displace elements by a given displacement
×
NEW
126
 */
×
127
const displaceElements = (displacement: { x: number; y: number }) => (element: ExcalidrawElement) => ({
×
128
  ...element,
129
  x: element.x + displacement.x,
130
  y: element.y + displacement.y,
131
});
132

×
133
const mergeWhiteboard = async (whiteboardApi: ExcalidrawImperativeAPI, whiteboardContent: string) => {
134
  const { hashElementsVersion, CaptureUpdateAction } = await lazyImportWithErrorHandler<ExcalidrawUtils>(
135
    () => import('@alkemio/excalidraw')
136
  );
137

138
  let parsedWhiteboard: unknown;
×
139
  try {
×
140
    parsedWhiteboard = JSON.parse(whiteboardContent);
×
141
  } catch (err) {
×
142
    throw new WhiteboardMergeError(`Unable to parse whiteboard content: ${err}`);
×
143
  }
144

145
  if (!isWhiteboardLike(parsedWhiteboard)) {
×
146
    throw new WhiteboardMergeError('Whiteboard verification failed');
147
  }
148

149
  try {
150
    // Insert missing files into current whiteboard:
151
    const currentFiles = whiteboardApi.getFiles();
152
    for (const fileId in parsedWhiteboard.files) {
153
      if (!currentFiles[fileId]) {
154
        whiteboardApi.addFiles([parsedWhiteboard.files[fileId]]);
155
      }
×
156
    }
157

158
    const currentElements = whiteboardApi.getSceneElementsIncludingDeleted();
159
    const sceneVersion = hashElementsVersion(whiteboardApi.getSceneElementsIncludingDeleted());
160

161
    const currentElementsBBox = getBoundingBox(currentElements);
×
162
    const insertedWhiteboardBBox = getBoundingBox(parsedWhiteboard.elements);
163
    const displacement = calculateInsertionPoint(currentElementsBBox, insertedWhiteboardBBox);
164

×
165
    const replacedIds: Record<string, string> = {};
166
    // fractional indices does not need overwriting
167
    const insertedElements = parsedWhiteboard.elements
×
168
      ?.map(generateNewIds(replacedIds))
×
169
      .map(replaceElementVersion(sceneVersion + 1))
170
      .map(replaceBoundElementsIds(replacedIds))
×
171
      .map(displaceElements(displacement));
172

173
    const newElements = [...currentElements, ...insertedElements];
×
174
    whiteboardApi.updateScene({
175
      elements: newElements,
×
176
      captureUpdate: CaptureUpdateAction.IMMEDIATELY,
177
    });
×
178

×
179
    if (insertedElements.length > 0) {
×
180
      whiteboardApi.scrollToContent(insertedElements, {
×
181
        animate: true,
182
        fitToViewport: true,
183
        duration: ANIMATION_SPEED,
184
        viewportZoomFactor: ANIMATION_ZOOM_FACTOR,
×
185
      });
×
186
    }
187

×
188
    return true;
×
189
  } catch (err) {
×
190
    throw new WhiteboardMergeError(`Unable to merge whiteboards: ${err}`);
191
  }
×
192
};
193

×
194
export default mergeWhiteboard;
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc