Create previous note + last note links for meetings

Things I have tried

I have created a dataview query which almost does what I want - but not quite…

list " <<< Previous Meeting"
   where project ="Test"
   where file.ctime < this.file.ctime
   sort file.ctime desc
   limit 1

What I’m trying to do

I make a lot of meeting notes. I work for clients; they have several projects. In these projects there are subjects to discuss, and usually repeating meetings. I make a note for each meeting.

I want to create a template where, at the bottom of each meeting note, I can jump back to the preceding note - or forward to the next. That way, I can ‘scroll’ through my notes quickly, as I sometimes need to look something up.

Ideally, I would have a line on the bottom like this:

<<< Previous Note “some…empty…space” Next Note >>>

So, 1 line, without the ‘ugly’ link that LIST shows ; with BOTH of the links. The query I made (of course I would use metadata in the where clause, just a prototype for now) is a bit inefficient, but works; but has several problems. First, I cannot substitute text for the link (or maybe I just do not know how to do this). i can only add text, which is not what I want. I just want a “previous” and “next” link. Second, I cannot get BOTH those links on the same line. Third, even if I could, I do not know how to allign them.

I suspect it can be done with dataviewjs, but dataview and even obsidian are still rather new to me - I cannot find where to start. I am familiar with queries etc - so I can customise when someone can point me in the right direction.

Help is appreciated!

Nice query! To have it show " <<< Previous Meeting" as a link, rather than the link and previous meeting separately, use LIST WITHOUT ID (caps doesn’t matter) to hide the first link and link(file.path, " <<< Previous Meeting") where you currently have the " <<< Previous Meeting" string in your query.

However, I don’t know how to get two queries on the same line with dataview query language, so I agree with you that dataviewjs may be the answer. Here’s a translation of your query above from Dataview Query Language (DQL) to dataviewjs, then displaying the answer in a dv.paragraph since you aren’t looking for either a list, table, or taskList.

const prevMeetingPath = dv.pages().where(p => p.project === "Test" && p.file.ctime < dv.current().file.ctime).sort((p => p.file.ctime), "desc").limit(1).map(p => p.file.path);
const nextMeetingPath = // fill in the version for your next meeting query
const combinedLine = dv.fileLink(prevMeetingPath, false, " <<< Previous Meeting") + "             " + dv.fileLink(nextMeetingPath, false, "Next Meeting >>>");
dv.paragraph(combinedLine);

Notes:

  • The equivalent for this in dataviewjs is dv.current().
  • I just put some random amount of space in the middle of the combinedLine between the " ", you can adjust.
  • The false in the middle of the dv.fileLink calls says to not embed the notes, just link to them.

Good luck!

Oh this is great!

I can build on this. When I have the definitive query I will post it here. For now, I go with:

const prevMeetingPath = dv.pages().where(p => p.project === "Test" && p.file.ctime < dv.current().file.ctime).sort((p => p.file.ctime), "desc").limit(1).map(p => p.file.path); 
const nextMeetingPath = dv.pages().where(p => p.project === "Test" && p.file.ctime > dv.current().file.ctime).sort((p => p.file.ctime), "asc").limit(1).map(p => p.file.path); 
const combinedLine = dv.fileLink(prevMeetingPath, false, " <<< Previous Meeting") + "<span style=float:right;>" + dv.fileLink(nextMeetingPath, false, "Next Meeting >>>" ) ;
dv.paragraph(combinedLine);

which gives me the proper previous/next options, 1 line, alligned left and right

Ok, I did some finetuning, and now my query looks like this:

var prevMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime < dv.current().file.ctime).sort((p => p.file.ctime), 
    "desc").limit(1).map(p => p.file.path); 
var nextMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime > dv.current().file.ctime).sort((p => p.file.ctime), 
    "asc").limit(1).map(p => p.file.path); 

var combinedLine = "";

if (prevMeetingPath != "") {
	combinedLine = combinedLine + dv.fileLink(prevMeetingPath, false, " <<< Previous Meeting");
	}
	else {
	combinedLine = combinedLine + " <<< No Previous Meeting";
	}

combinedLine = combinedLine +	 "<span style=float:right;>"
	
if (nextMeetingPath != "") {
	combinedLine = combinedLine + dv.fileLink(nextMeetingPath, false, "Next Meeting >>>");  
	}
	else {
	combinedLine = combinedLine + "No Next Meeting >>>";
	}

dv.paragraph(combinedLine);

Maybe I will replace this with real buttons later, but for now this works great.

2 Likes

Very nice! Thanks for code catching the cases where the previous or next meetings don’t exist - that’s important and I totally forgot about it! You should mark your previous post as the answer.
One additional thing to look into, if you think you might make changes later: dataviewjs allows you to to put some code in a separate .js file and then import it into a dataviewjs block with dv.view(...) - documentation here. You can pass arguments (e.g. dv.current() if you had a parameter currentPage or something like that) so that each call gets different results. Upside: If you put the code using dv.view(...) in all your meeting notes and then want to change the thing to buttons, you only have to make the change to the one spot in your .js file and all the meeting note pages would change. Downside: You can’t edit .js files in Obsidian, so you’d need to open that in a separate text editor.

Oh, that is nice! Like an #include.

Already implemented this, works great! The link says I have to add an ‘await’ but seems to be working fine without it.

1 Like

I don’t know enough JS to understand how await on the last line of a code block works. I know that if you (for some reason) were saving the result of your dv.view(...) call into a variable for further manipulation you would definitely want the await! If you find out anything further about this situation, I am curious to learn!

The last update of Dataview (0.5.36) broke something!
This query suddenly stopped working…

Evaluation Error: TypeError: this.path.replace is not a function
    at Link.obsidianLink (plugin:dataview:9315:35)

I traced it back to the .fileLink() function. It seems that before, this function could handle links to ("[thisisalink]" and now it cannot. I found a workaround, published below (although I do not fully understand what first() does, I saw this in a reply from blacksmithgu, so who am i to argue :slight_smile: and it works.

// depends on organisation, project and meetingid 
// Used in Meeting Template
// There are variants with only organisation and project

var prevMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime < dv.current().file.ctime).sort((p => p.file.ctime), 
    "desc")
    .limit(1)
    .map(p => p.file.path); 
var nextMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime > dv.current().file.ctime).sort((p => p.file.ctime), 
    "asc")
    .limit(1)
    .map(p => p.file.path); 

var combinedLine = "";

// dv.paragraph(dv.pages().where(p => 	p.organisation === v.current().organisation && 	p.project === dv.current().project));
	
if (prevMeetingPath.first()) {
	combinedLine = combinedLine + dv.fileLink(prevMeetingPath.first(), false, " <<< Previous Meeting");
	}
	else {
	combinedLine = combinedLine + " <<< No Previous Meeting";
	}

combinedLine = combinedLine +	 "<span style=float:right;>"
	
if (nextMeetingPath.first()) {
	combinedLine = combinedLine + dv.fileLink(nextMeetingPath.first(), false, "Next Meeting >>>");  
	}
	else {
	combinedLine = combinedLine + "No Next Meeting >>>";
	}

dv.paragraph(combinedLine);
1 Like

first() takes the first element from a list or array. (There is also a last().) Even though limit(1) meant your list had only one thing in it, dataview was still treating it like a list. One thing you could do would be to put the call to first(() at the end of the chain after limit and map (for both variables!), rather than having to repeat it multiple times down in the second half of your code. That would be less confusing for future-me looking back at this code in the future, I think, but it depends what is clear for you. Glad you got a solution!

I started out by trying to put the first in the initial selection, as that is of course the cleaner solution. Alas, I cannot get that to work, I do not know the right syntax.

I tried

.limit(1)
.map(p => p.file.path)
.first();

and also

.limit(1)
.map(p => p.file.path.first());

but both do not work (unknown function). I probably need to start looking into the javascript syntax, never done any work and it’s frustrating chasing the right syntax.

Huh, I would have expected the first one you showed to work! I am confused! I will see if I can unconfuse myself, and then edit this post. Apologies for suggesting something that didn’t work!

It looks strange, right? But is really does not work, ‘file.path.first()’ is not a function.

If I do it in two parts, it does work:

var prevMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime < dv.current().file.ctime).sort((p => p.file.ctime), 
    "desc")
    .limit(1)
    .map(p => p.file.path);
	
var nextMeetingPath = dv.pages().where(p => 
	p.organisation === dv.current().organisation && 
	p.project === dv.current().project && 
	p.meetingid === dv.current().meetingid && 
	p.file.ctime > dv.current().file.ctime).sort((p => p.file.ctime), 
    "asc")
    .limit(1)
    .map(p => p.file.path); 

var prevMeetingPathString = prevMeetingPath.first();
var nextMeetingPathString = nextMeetingPath.first();
var combinedLine = "";

if (prevMeetingPathString) {
	combinedLine = combinedLine + dv.fileLink(prevMeetingPathString, false, " <<< Previous Meeting");
	}
	else {
	combinedLine = combinedLine + " <<< No Previous Meeting";
	}

combinedLine = combinedLine +	 "<span style=float:right;>"
	
if (nextMeetingPathString)  {
	combinedLine = combinedLine + dv.fileLink(nextMeetingPathString, false, "Next Meeting >>>");  
	}
	else {
	combinedLine = combinedLine + "No Next Meeting >>>";
	}

dv.paragraph(combinedLine);

Yes, very nice investigating! I have at least some understanding of my mistake now.

The thing I keep forgetting, which is what I forgot when I thought you could add first() on to the chain after map() above: map does not change/mutate the thing it is looking through; instead it returns a new copy with the results of the transformation. This is different from where(), sort(), limit() which all change the thing they are operating on, and also return that same thing. Dataview’s DataArray objects (also known as the “thing” we are operating on or looking through here), have a version of map called mutate that does the change-itself-in-place behavior that would allow you to keep adding on, just like sort() and limit(). (If you want to experiment, replace map with mutate in your code (the stuff inside the parentheses stays the same) and see if you can add .first() afterwards then.)

Some trivia, since you expressed interest in javascript syntax: since your map is only pulling one piece of information out per-page, you could replace the call to map with just accessing that directly. ...limit(1).file.path (or equivalently: ...limit(1)["file"]["path"] - that’s the javascript syntax trivia, two different ways to access a named field in an object, and dataview’s DataArrays are objects, which just also can behave like arrays. One of the fancy features of DataArrays is that you can do these field accesses and still have your end result array-ified, which means we still need .first() here. But now we can add it right on to the end of the chain: ...limit(1).file.path.first(). (I tested it this time, to be sure!) Hopefully we’ve both learned something now, and maybe I’ll stop making that mistake with the makes-a-new-copy functionality of map!