Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing Bugs and introduce better library support in canvas note #787

Merged
merged 9 commits into from
Dec 17, 2024
2 changes: 1 addition & 1 deletion src/becca/entities/battachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {

/** @returns true if the note has string content (not binary) */
hasStringContent(): boolean {
return this.type !== undefined && utils.isStringNote(this.type, this.mime);
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
}

isContentAvailable() {
Expand Down
96 changes: 80 additions & 16 deletions src/public/app/widgets/type_widgets/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import libraryLoader from '../../services/library_loader.js';
import TypeWidget from './type_widget.js';
import utils from '../../services/utils.js';
import linkService from '../../services/link.js';

import server from '../../services/server.js';
const TPL = `
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
<style>
Expand Down Expand Up @@ -115,6 +115,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
this.reactHandlers; // used to control react state

this.libraryChanged = false;

// 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.
//every libraryitem is saved on its own json file in the attachments of the note.
this.librarycache = [];
this.attachmentMetadata=[]
}

static getType() {
Expand Down Expand Up @@ -236,23 +241,47 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
fileArray.push(file);
}

Promise.all(
(await note.getAttachmentsByRole('canvasLibraryItem'))
.map(async attachment => {
const blob = await attachment.getBlob();
return {
blob, // Save the blob for libraryItems
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
attachmentId: attachment.attachmentId,
title: attachment.title,
},
};
})
).then(results => {
if (note.noteId !== this.currentNoteId) {
// current note changed in the course of the async operation
return;
}

// Extract libraryItems from the blobs
const libraryItems = results
.map(result => result.blob.getJsonContentSafely())
.filter(item => !!item);

// Extract metadata for each attachment
const metadata = results.map(result => result.metadata);

// Update the library and save to independent variables
this.excalidrawApi.updateLibrary({ libraryItems, merge: false });

// save state of library to compare it to the new state later.
this.librarycache = libraryItems;
this.attachmentMetadata = metadata;
});

// Update the scene
this.excalidrawApi.updateScene(sceneData);
this.excalidrawApi.addFiles(fileArray);
this.excalidrawApi.history.clear();
}

Promise.all(
(await note.getAttachmentsByRole('canvasLibraryItem'))
.map(attachment => attachment.getBlob())
).then(blobs => {
if (note.noteId !== this.currentNoteId) {
// current note changed in the course of the async operation
return;
}

const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
this.excalidrawApi.updateLibrary({libraryItems, merge: false});
});



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

// 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.
//We need the cache to delete old attachments later in the server.

const libraryItemsMissmatch = this.librarycache.filter(obj1 => !libraryItems.some(obj2 => obj1.id === obj2.id));


// 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.
// 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)
// then we combine its id and title and search the according attachmentID.

const matchingItems = this.attachmentMetadata.filter(meta => {
// Loop through the second array and check for a match
return libraryItemsMissmatch.some(item => {
// Combine the `name` and `id` from the second array
const combinedTitle = `${item.id}${item.name}`;
return meta.title === combinedTitle;
});
});

// we save the attachment ID`s in a variable and delete every attachmentID. Now the items that the user deleted will be deleted.
const attachmentIds = matchingItems.map(item => item.attachmentId);



//delete old attachments that are no longer used
for (const item of attachmentIds){

await server.remove(`attachments/${item}`);

}

let position = 10;

// prepare data to save to server e.g. new library items.
for (const libraryItem of libraryItems) {

attachments.push({
role: 'canvasLibraryItem',
title: libraryItem.id,
title: libraryItem.id + libraryItem.name,
mime: 'application/json',
content: JSON.stringify(libraryItem),
position: position

});

position += 10;
}

}

return {
Expand Down
69 changes: 69 additions & 0 deletions src/services/search/expressions/note_content_fulltext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import striptags from "striptags";
import utils from "../../utils.js";
import sql from "../../sql.js";


const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%='];

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

content = content.replace(/&nbsp;/g, ' ');
}
else if (type === 'mindMap' && mime === 'application/json') {

let mindMapcontent = JSON.parse (content);

// Define interfaces for the JSON structure
interface MindmapNode {
id: string;
topic: string;
children: MindmapNode[]; // Recursive structure
direction?: number;
expanded?: boolean;
}

interface MindmapData {
nodedata: MindmapNode;
arrows: any[]; // If you know the structure, replace `any` with the correct type
summaries: any[];
direction: number;
theme: {
name: string;
type: string;
palette: string[];
cssvar: Record<string, string>; // Object with string keys and string values
};
}

// Recursive function to collect all topics
function collectTopics(node: MindmapNode): string[] {
// Collect the current node's topic
let topics = [node.topic];

// If the node has children, collect topics recursively
if (node.children && node.children.length > 0) {
for (const child of node.children) {
topics = topics.concat(collectTopics(child));
}
}

return topics;
}


// Start extracting from the root node
const topicsArray = collectTopics(mindMapcontent.nodedata);

// Combine topics into a single string
const topicsString = topicsArray.join(", ");


content = utils.normalize(topicsString.toString());
}
else if (type === 'canvas' && mime === 'application/json') {
interface Element {
type: string;
text?: string; // Optional since not all objects have a `text` property
id: string;
[key: string]: any; // Other properties that may exist
}

let canvasContent = JSON.parse (content);
const elements: Element [] = canvasContent.elements;
const texts = elements
.filter((element: Element) => element.type === 'text' && element.text) // Filter for 'text' type elements with a 'text' property
.map((element: Element) => element.text!); // Use `!` to assert `text` is defined after filtering

content =utils.normalize(texts.toString())
}


return content.trim();
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const STRING_MIME_TYPES = [
"image/svg+xml"
];

function isStringNote(type: string | null, mime: string) {
function isStringNote(type: string | undefined, mime: string) {
// render and book are string note in the sense that they are expected to contain empty string
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type))
|| mime.startsWith('text/')
Expand Down
2 changes: 1 addition & 1 deletion src/share/shaca/entities/sattachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class SAttachment extends AbstractShacaEntity {

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

getPojo() {
Expand Down