mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-10-03 11:21:31 +08:00
Merge pull request #787 from CobriMediaJulien/develop
Fixing Bugs and introduce better library support in canvas note
This commit is contained in:
commit
e7711ce408
@ -98,7 +98,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
|||||||
|
|
||||||
/** @returns true if the note has string content (not binary) */
|
/** @returns true if the note has string content (not binary) */
|
||||||
hasStringContent(): boolean {
|
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() {
|
isContentAvailable() {
|
||||||
|
@ -2,7 +2,7 @@ import libraryLoader from '../../services/library_loader.js';
|
|||||||
import TypeWidget from './type_widget.js';
|
import TypeWidget from './type_widget.js';
|
||||||
import utils from '../../services/utils.js';
|
import utils from '../../services/utils.js';
|
||||||
import linkService from '../../services/link.js';
|
import linkService from '../../services/link.js';
|
||||||
|
import server from '../../services/server.js';
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
||||||
<style>
|
<style>
|
||||||
@ -115,6 +115,11 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
|||||||
this.reactHandlers; // used to control react state
|
this.reactHandlers; // used to control react state
|
||||||
|
|
||||||
this.libraryChanged = false;
|
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() {
|
static getType() {
|
||||||
@ -236,24 +241,48 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
|
|||||||
fileArray.push(file);
|
fileArray.push(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.excalidrawApi.updateScene(sceneData);
|
|
||||||
this.excalidrawApi.addFiles(fileArray);
|
|
||||||
this.excalidrawApi.history.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
(await note.getAttachmentsByRole('canvasLibraryItem'))
|
(await note.getAttachmentsByRole('canvasLibraryItem'))
|
||||||
.map(attachment => attachment.getBlob())
|
.map(async attachment => {
|
||||||
).then(blobs => {
|
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) {
|
if (note.noteId !== this.currentNoteId) {
|
||||||
// current note changed in the course of the async operation
|
// current note changed in the course of the async operation
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryItems = blobs.map(blob => blob.getJsonContentSafely()).filter(item => !!item);
|
// Extract libraryItems from the blobs
|
||||||
this.excalidrawApi.updateLibrary({libraryItems, merge: false});
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// set initial scene version
|
// set initial scene version
|
||||||
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
|
if (this.currentSceneVersion === this.SCENE_VERSION_INITIAL) {
|
||||||
this.currentSceneVersion = this.getSceneVersion();
|
this.currentSceneVersion = this.getSceneVersion();
|
||||||
@ -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
|
// there's no separate method to get library items, so have to abuse this one
|
||||||
const libraryItems = await this.excalidrawApi.updateLibrary({merge: true});
|
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;
|
let position = 10;
|
||||||
|
|
||||||
|
// prepare data to save to server e.g. new library items.
|
||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
|
|
||||||
attachments.push({
|
attachments.push({
|
||||||
role: 'canvasLibraryItem',
|
role: 'canvasLibraryItem',
|
||||||
title: libraryItem.id,
|
title: libraryItem.id + libraryItem.name,
|
||||||
mime: 'application/json',
|
mime: 'application/json',
|
||||||
content: JSON.stringify(libraryItem),
|
content: JSON.stringify(libraryItem),
|
||||||
position: position
|
position: position
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
position += 10;
|
position += 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -12,6 +12,7 @@ import striptags from "striptags";
|
|||||||
import utils from "../../utils.js";
|
import utils from "../../utils.js";
|
||||||
import sql from "../../sql.js";
|
import sql from "../../sql.js";
|
||||||
|
|
||||||
|
|
||||||
const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%='];
|
const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%='];
|
||||||
|
|
||||||
const cachedRegexes: Record<string, RegExp> = {};
|
const cachedRegexes: Record<string, RegExp> = {};
|
||||||
@ -133,6 +134,74 @@ class NoteContentFulltextExp extends Expression {
|
|||||||
|
|
||||||
content = content.replace(/ /g, ' ');
|
content = content.replace(/ /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();
|
return content.trim();
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ const STRING_MIME_TYPES = [
|
|||||||
"image/svg+xml"
|
"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
|
// 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))
|
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type))
|
||||||
|| mime.startsWith('text/')
|
|| mime.startsWith('text/')
|
||||||
|
@ -62,7 +62,7 @@ class SAttachment extends AbstractShacaEntity {
|
|||||||
|
|
||||||
/** @returns true if the attachment has string content (not binary) */
|
/** @returns true if the attachment has string content (not binary) */
|
||||||
hasStringContent() {
|
hasStringContent() {
|
||||||
return utils.isStringNote(null, this.mime);
|
return utils.isStringNote(undefined, this.mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPojo() {
|
getPojo() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user