zadam / trilium

Build your personal knowledge base with Trilium Notes
GNU Affero General Public License v3.0
27.2k stars 1.9k forks source link

@alwaysVisible notes #715

Open nil0x42 opened 4 years ago

nil0x42 commented 4 years ago

Hi have some 'saved search' notes which are a little complex. Sometimes i want to search with them in my whole notes database, but sometimes, i need to run these searches only within the context of my 'hoisted' tree. Therefore, those saved searches are stored at root, so i have to unhoist, go to the search, add a noteid filter to my search script handler (the noteid of my previously hoisted note), and run the search. Then i am afraid of hoisting again, because the list returned by my search will disapear again.

Anyway, for global flexibility, it might be interesting to implement a @unhoistable label.

Of course this triggers other problems, such as what to do if a random note within a long path of note is unhoistable, then how to display the tree when hoisting. May be it can fit with issue 596 Consider implementing something like bookmarks, if such bookmarks are unhoistable by default.

zadam commented 4 years ago

Could you explain what exactly @unhoistable should do? I do understand the problem you described but not really this label ...

nil0x42 commented 4 years ago

If i hoist my "Dev" note, then only it's child appear on the tree. the tag i mention would force notes to be visible at root even if it's not part of the hoisted note subtree.

Maybe a label name like alwaysVisible would be more obvious

Example hoising Dev note now:

Dev
  C
    Templates
    Debug
  Python
    Scripts

Same thing, with Github Pages note labeled with AlwaysVisible (or unhoistable):

Github Pages
Dev
  C
    Templates
    Debug
  Python
    Scripts

In this example, Github Pages is a saved search looking for: @pageUrl=*https://github.com/. When my tree is not hoisted, it shows all pages saved to the whole trilium database. But now that i'm hoisting into Dev, it will only show notes paching the search that are part of the Dev subtree (because i'm hoising).

Of course for this specific case, i could clone my save search Github Pages into Dev, but having it always displayed at top allow be to use my pre-built search query within anything i wish to hoist.

jaroet commented 4 years ago

For what it is worth ... I like this idea. Although I would not show it in the tree (confusing) but in the sidebar. A separate category like the current 'what links here' but then something like 'alwaysVisible' where all notes with that attribute are shown.

For me, I would like to see my 'working on ...' note to be visible always so I can always quickly go to that. Although the search might be OK for that too. That saved searches with the attribute alwaysVisible will only search in hoisted tree's is a great addition also.

nil0x42 commented 4 years ago

Good idea, a widget showing some specific notes is a good idea.

Therefore, in the case of saved searches, it becomes more interesting to have them shown in the tree IMHO because a saved search shows children, and displaying it in a widget would be heavy, requiring to reimplement the tree system in a widget

jhc86 commented 4 years ago

I like the idea of a widget. I tried to make one (attached) and added it to the list of widgets in sidebar.js, but it didn't show up for me. I'd love to learn how to make this work.

always_available.js.txt

zadam commented 4 years ago

@jhc86 attaching fixed custom script. Unfortunately this is not well documented so far, but the custom script needs to follow a little bit different pattern than the built-in ones:

class AlwaysVisibleWidget extends api.StandardWidget {
    getWidgetTitle() { return "labelled with alwaysVisible"; }

    getPosition() { return 0; }

    isEnabled() { return true; }

    getMaxHeight() { return "200px"; }

    async doRenderBody() {
        const filteredNotes = await api.searchForNotes('@alwaysVisible');

        if (filteredNotes.length === 0) {
            this.$body.text("No notes labelled 'alwaysVisible' yet ...");
            return;
        }

        const $list = $("<ul>");
        let i = 0;

        for (; i < filteredNotes.length && i < 50; i++) {
            const note = filteredNotes[i];

            const $item = $("<li>").append(await api.createNoteLink(note.noteId));

            $list.append($item);
        }

        if (i < filteredNotes.length) {
            $list.append($("<li>").text(`${filteredNotes.length - i} more notes ...`))
        }

        this.$body.empty().append($list);
    }
}

module.exports = AlwaysVisibleWidget;

However on it's own it's just a code note. To be active, it needs to be attached through widget relation on a note on which you want to display it. If you want to display this widget on all notes, you can put this relation on root and set it as "inheritable".

jhc86 commented 4 years ago

@zadam This works perfect and is very helpful, thank you!!

jaroet commented 4 years ago

Thank you ... works fine for me also.

At first it did not work but I found you need to set the codenote to JS Frontend or JS Backend syntax (does it matter? Both work). Quite obvious afterwards but it took me 5 minutes :-)

Op do 28 nov. 2019 om 00:45 schreef jhc86 notifications@github.com:

@zadam https://github.com/zadam This works perfect and is very helpful, thank you!!

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/zadam/trilium/issues/715?email_source=notifications&email_token=AG3CDWFR2DPOMG46STKQRH3QV4BDBA5CNFSM4JQEMWM2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFLA4GA#issuecomment-559287832, or unsubscribe https://github.com/notifications/unsubscribe-auth/AG3CDWCM2IH4O5JVX3EJRYTQV4BDBANCNFSM4JQEMWMQ .

nil0x42 commented 4 years ago

I liked that widget, i started using it. Therefore, it does not address the issue related to being able to use Saved Searches within the scope of a hoisted note.

The simplest way to do it, would be to have some notes marked in such way that they are always show on root of the tree, even we are currently hoisting somewhere else.

For example, i have an Important Cheat Sheets saved search, that displays all notes having @important & @cheatsheet labels. That note is at the root of my tree. If i click on it, it displays all matching notes, as expected.

But now let's imagine i hoist into System -> Windows -> Process Injection. Now i'd like to display all important cheatsheets that are part of my hoisted note (Process Injection here). From now, there are two ways of achieving that:

  1. having a clone of my saved search to the root of Process Injection. But the con is that i would have to potentially clone that note as a child of every note i hoist into.
  2. using the search bar to manually type @important @cheatsheet. The con here, is that is becomes impossible when the search becomes more complex, like in the case of a saved search with script relation

That's why having the possibility to force a note being always displayed at tree top, even while hoising, would help a lot in such case

zadam commented 4 years ago

I understand that the widget does not help your use case.

However I'm not sure about the proposed solution - it does not feel conceptually correct - e.g. on which position should these @alwaysVisible notes be displayed under the root - how would they mix with notes which are really present under the hoisted tree?

My thinking on how to cover such use cases is instead going into a separate view - e.g. in a widget, but with a tree widget like on the left sidebar.

My proposal - configurable "Saved search widget" which displays in a tree results of a particular search query. That search query can be e.g. @alwaysVisible - result of that would in such case be your Important Cheat Sheets saved search which you can expand (since it's a tree widget) and see results.

There's a problem with this since the "Saved search widget" must search globally, even outside current hoisting, but then your Important Cheat Sheets should search in the scope of hoisted note, but that could be potentially solved by some search modifier (@global or something).

nil0x42 commented 4 years ago

Your proposal sounds quite interesting, and limits possible confusion.

Here is a screenshot of how i had imagined the feature (in this case, i'm hoising into my 'trilium' note, and below the horizontal bar, my saved searches, always visible, allow me to perform my searches within the context of my hoisted note) x

I might have misunderstood, but would your proposal support having multiple saved searches available ?

zadam commented 4 years ago

I might have misunderstood, but would your proposal support having multiple saved searches available ?

Yes, widget's saved search would define what to display in the widget as top level notes - which can be any kind of notes including your saved search notes.

nil0x42 commented 4 years ago

That feature would be great !, i really need being able to filter by note types within the context of an hoisted note.

jhc86 commented 4 years ago

If anyone still wants to play around with this widget here it is with buttons to easily label and unlabel notes (add and remove them from the widget) and it is also adjusted to use the backend and shows the notes when hoisted

  // This was buggy, update has been posted below
nil0x42 commented 4 years ago

thank you

jaroet commented 4 years ago

With the script jhc86 gave it looks like all labels are removed from a note when you use the remove label button. In my case it was a theme notes I temporary accessed a lot. When I used the remove button the appTheme label was also removed.

Regards, JeRoen

jhc86 commented 4 years ago

Thank you for catching this @jaroet ! I might misunderstand how removeLabel() works. I've replaced it and did a (little) test and I think it will work OK now.


class AlwaysVisibleWidget extends api.StandardWidget {
    getWidgetTitle() { return "labelled with alwaysVisible"; }

    getPosition() { return 0; }

    isEnabled() { return true; }

    getMaxHeight() { return "200px"; }

    async addCurrentNoteAndRender() {
        if (this.listeningToEvents) {
            this.listeningToEvents = false;
            this.button.classList.add('disabled')
            this.button.innerText = 'labelling...'
            await api.runOnServer(async (noteId) => {
                const n = await api.getNote(noteId)
                await n.setLabel('alwaysVisible')
            console.log(0, noteId);
            }, [originEntity.noteId])
            this.doRenderBody();
        }
    }

    async isNoteLabelledAlwaysVisible(noteId) {
        return await api.runOnServer(async (noteId) => {
            const n = await api.getNote(noteId)
            return await n.hasLabel('alwaysVisible')
        }, [noteId])
    }

    async removeNoteLabellingAndRender(noteId, callingButton) {
        if (this.listeningToEvents) {
            this.listeningToEvents = false;
            callingButton.classList.add('disabled')
            callingButton.innerText = 'removing...'
            await api.runOnServer(async (noteId) => {
                const n = await api.getNote(noteId)
                //await n.removeLabel('alwaysVisible')
                const l = await n.getLabel('alwaysVisible')
                l.isDeleted = true;
                await l.save();
            console.log(1, noteId);
            }, [noteId])
            this.doRenderBody();
        }
    }

    async doRenderBody() {
        this.$body.text('loading...')
        const filteredNotes = await api.runOnServer(async () => {
            return await api.getNotesWithLabel('alwaysVisible');
        }, []);

        this.button = document.createElement('button');
        this.button.classList.add('btn', 'btn-secondary', 'btn-sm')
        this.button.style = 'position: absolute; top: 1px; right: 10px;'
        this.button.append(document.createTextNode('+ label current'));
        let boundAddCurrentNoteAndRender = this.addCurrentNoteAndRender.bind(this)
        this.button.addEventListener('click', boundAddCurrentNoteAndRender);
        this.$body.empty()
        if (!await this.isNoteLabelledAlwaysVisible(originEntity.noteId)) {
            this.$body.append(this.button)
        }
        if (filteredNotes.length === 0) {
            this.$body.append(document.createTextNode(
                "No notes labelled 'alwaysVisible' yet ..."))

            return;
        }
        const $list = $("<ul>");
        let i = 0;

        for (; i < filteredNotes.length && i < 50; i++) {
            const note = filteredNotes[i];

            const unlabelButton = document.createElement('button')
            unlabelButton.innerText = '×';
            unlabelButton.classList.add('btn', 'btn-xs')
            unlabelButton.style = 'padding: 0px 4px;'
            unlabelButton.addEventListener('click',
                this.removeNoteLabellingAndRender.bind(this, note.noteId,                             unlabelButton))
            const $item = $("<li>")
            if (note.noteId === originEntity.noteId) {
            $item.append(document.createTextNode(note.title+' '))
                                   .append(unlabelButton);
            } else {
            $item.append(await api.createNoteLink(note.noteId))
                                   .append(document.createTextNode(' '))
                                   .append(unlabelButton);
            }
            $list.append($item);
        }

        if (i < filteredNotes.length) {
            $list.append($("<li>").text(`${filteredNotes.length - i} more notes ...`))
        }
        console.log('appending list');
        this.$body.append($list)
        this.listeningToEvents = true;
    }
}

module.exports = AlwaysVisibleWidget;
jaroet commented 4 years ago

thank you @jhc86 for the quick fix ... works fine now in my small test. I like the button in the header of the widget 👍