Skip to content

Commit e7711ce

Browse files
authored
Merge pull request #787 from CobriMediaJulien/develop
Fixing Bugs and introduce better library support in canvas note
2 parents aa5f1c9 + c419818 commit e7711ce

File tree

5 files changed

+152
-19
lines changed

5 files changed

+152
-19
lines changed

src/becca/entities/battachment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
9898

9999
/** @returns true if the note has string content (not binary) */
100100
hasStringContent(): boolean {
101-
return this.type !== undefined && utils.isStringNote(this.type, this.mime);
101+
return utils.isStringNote(this.type, this.mime); // here was !== undefined && utils.isStringNote(this.type, this.mime); I dont know why we need !=undefined. But it filters out canvas libary items
102102
}
103103

104104
isContentAvailable() {

src/public/app/widgets/type_widgets/canvas.js

+80-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import libraryLoader from '../../services/library_loader.js';
22
import TypeWidget from './type_widget.js';
33
import utils from '../../services/utils.js';
44
import linkService from '../../services/link.js';
5-
5+
import server from '../../services/server.js';
66
const TPL = `
77
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
88
<style>
@@ -115,6 +115,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
115115
this.reactHandlers; // used to control react state
116116

117117
this.libraryChanged = false;
118+
119+
// these 2 variables are needed to compare the library state (all library items) after loading to the state when the library changed. So we can find attachments to be deleted.
120+
//every libraryitem is saved on its own json file in the attachments of the note.
121+
this.librarycache = [];
122+
this.attachmentMetadata=[]
118123
}
119124

120125
static getType() {
@@ -236,23 +241,47 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
236241
fileArray.push(file);
237242
}
238243

244+
Promise.all(
245+
(await note.getAttachmentsByRole('canvasLibraryItem'))
246+
.map(async attachment => {
247+
const blob = await attachment.getBlob();
248+
return {
249+
blob, // Save the blob for libraryItems
250+
metadata: { // metadata to use in the cache variables for comparing old library state and new one. We delete unnecessary items later, calling the server directly
251+
attachmentId: attachment.attachmentId,
252+
title: attachment.title,
253+
},
254+
};
255+
})
256+
).then(results => {
257+
if (note.noteId !== this.currentNoteId) {
258+
// current note changed in the course of the async operation
259+
return;
260+
}
261+
262+
// Extract libraryItems from the blobs
263+
const libraryItems = results
264+
.map(result => result.blob.getJsonContentSafely())
265+
.filter(item => !!item);
266+
267+
// Extract metadata for each attachment
268+
const metadata = results.map(result => result.metadata);
269+
270+
// Update the library and save to independent variables
271+
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });
272+
273+
// save state of library to compare it to the new state later.
274+
this.librarycache = libraryItems;
275+
this.attachmentMetadata = metadata;
276+
});
277+
278+
// Update the scene
239279
this.excalidrawApi.updateScene(sceneData);
240280
this.excalidrawApi.addFiles(fileArray);
241281
this.excalidrawApi.history.clear();
242282
}
243-
244-
Promise.all(
245-
(await note.getAttachmentsByRole('canvasLibraryItem'))
246-
.map(attachment => attachment.getBlob())
247-
).then(blobs => {
248-
if (note.noteId !== this.currentNoteId) {
249-
// current note changed in the course of the async operation
250-
return;
251-
}
252-
253-
const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
254-
this.excalidrawApi.updateLibrary({libraryItems, merge: false});
255-
});
283+
284+
256285

257286
// set initial scene version
258287
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
@@ -313,19 +342,54 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
313342
// there's no separate method to get library items, so have to abuse this one
314343
const libraryItems = await this.excalidrawApi.updateLibrary({merge: true});
315344

345+
// excalidraw saves the library as a own state. the items are saved to libraryItems. then we compare the library right now with a libraryitemcache. The cache is filled when we first load the Library into the note.
346+
//We need the cache to delete old attachments later in the server.
347+
348+
const libraryItemsMissmatch = this.librarycache.filter(obj1 => !libraryItems.some(obj2 => obj1.id === obj2.id));
349+
350+
351+
// before we saved the metadata of the attachments in a cache. the title of the attachment is a combination of libraryitem ´s ID und it´s name.
352+
// we compare the library items in the libraryitemmissmatch variable (this one saves all libraryitems that are different to the state right now. E.g. you delete 1 item, this item is saved as mismatch)
353+
// then we combine its id and title and search the according attachmentID.
354+
355+
const matchingItems = this.attachmentMetadata.filter(meta => {
356+
// Loop through the second array and check for a match
357+
return libraryItemsMissmatch.some(item => {
358+
// Combine the `name` and `id` from the second array
359+
const combinedTitle = `${item.id}${item.name}`;
360+
return meta.title === combinedTitle;
361+
});
362+
});
363+
364+
// we save the attachment ID`s in a variable and delete every attachmentID. Now the items that the user deleted will be deleted.
365+
const attachmentIds = matchingItems.map(item => item.attachmentId);
366+
367+
368+
369+
//delete old attachments that are no longer used
370+
for (const item of attachmentIds){
371+
372+
await server.remove(`attachments/${item}`);
373+
374+
}
375+
316376
let position = 10;
317377

378+
// prepare data to save to server e.g. new library items.
318379
for (const libraryItem of libraryItems) {
380+
319381
attachments.push({
320382
role: 'canvasLibraryItem',
321-
title: libraryItem.id,
383+
title: libraryItem.id + libraryItem.name,
322384
mime: 'application/json',
323385
content: JSON.stringify(libraryItem),
324386
position: position
387+
325388
});
326-
389+
327390
position += 10;
328391
}
392+
329393
}
330394

331395
return {

src/services/search/expressions/note_content_fulltext.ts

+69
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import striptags from "striptags";
1212
import utils from "../../utils.js";
1313
import sql from "../../sql.js";
1414

15+
1516
const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%='];
1617

1718
const cachedRegexes: Record<string, RegExp> = {};
@@ -133,6 +134,74 @@ class NoteContentFulltextExp extends Expression {
133134

134135
content = content.replace(/&nbsp;/g, ' ');
135136
}
137+
else if (type === 'mindMap' && mime === 'application/json') {
138+
139+
let mindMapcontent = JSON.parse (content);
140+
141+
// Define interfaces for the JSON structure
142+
interface MindmapNode {
143+
id: string;
144+
topic: string;
145+
children: MindmapNode[]; // Recursive structure
146+
direction?: number;
147+
expanded?: boolean;
148+
}
149+
150+
interface MindmapData {
151+
nodedata: MindmapNode;
152+
arrows: any[]; // If you know the structure, replace `any` with the correct type
153+
summaries: any[];
154+
direction: number;
155+
theme: {
156+
name: string;
157+
type: string;
158+
palette: string[];
159+
cssvar: Record<string, string>; // Object with string keys and string values
160+
};
161+
}
162+
163+
// Recursive function to collect all topics
164+
function collectTopics(node: MindmapNode): string[] {
165+
// Collect the current node's topic
166+
let topics = [node.topic];
167+
168+
// If the node has children, collect topics recursively
169+
if (node.children && node.children.length > 0) {
170+
for (const child of node.children) {
171+
topics = topics.concat(collectTopics(child));
172+
}
173+
}
174+
175+
return topics;
176+
}
177+
178+
179+
// Start extracting from the root node
180+
const topicsArray = collectTopics(mindMapcontent.nodedata);
181+
182+
// Combine topics into a single string
183+
const topicsString = topicsArray.join(", ");
184+
185+
186+
content = utils.normalize(topicsString.toString());
187+
}
188+
else if (type === 'canvas' && mime === 'application/json') {
189+
interface Element {
190+
type: string;
191+
text?: string; // Optional since not all objects have a `text` property
192+
id: string;
193+
[key: string]: any; // Other properties that may exist
194+
}
195+
196+
let canvasContent = JSON.parse (content);
197+
const elements: Element [] = canvasContent.elements;
198+
const texts = elements
199+
.filter((element: Element) => element.type === 'text' && element.text) // Filter for 'text' type elements with a 'text' property
200+
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering
201+
202+
content =utils.normalize(texts.toString())
203+
}
204+
136205

137206
return content.trim();
138207
}

src/services/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ const STRING_MIME_TYPES = [
160160
"image/svg+xml"
161161
];
162162

163-
function isStringNote(type: string | null, mime: string) {
163+
function isStringNote(type: string | undefined, mime: string) {
164164
// render and book are string note in the sense that they are expected to contain empty string
165165
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type))
166166
|| mime.startsWith('text/')

src/share/shaca/entities/sattachment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class SAttachment extends AbstractShacaEntity {
6262

6363
/** @returns true if the attachment has string content (not binary) */
6464
hasStringContent() {
65-
return utils.isStringNote(null, this.mime);
65+
return utils.isStringNote(undefined, this.mime);
6666
}
6767

6868
getPojo() {

0 commit comments

Comments
 (0)