for some reason, where does not seem to work anymore, but the equivalent filter does:

dv.taskList(dv.current().file.tasks.filter(t => !t.completed));
2 Likes

Fantastic. Thank you so much!

Hi all,

I am new to Obsidian and dataview.
I am currently investigating using dataviewjs to extract the notes contains a certain string in a field.
For example, I have to extract all the notes with Name field contains string “John”.
It might be Name :: John 123/ Name :: John abc/ Name :: Johnny

I am currently using

dv.list(dv.pages("").where(k => k.name.includes("John")))

But it prompted me the error:
Evaluation Error: TypeError: Cannot read property ‘includes’ of undefined at eval

May I have your help on this?
Thank You so much

I only use dataview (not in JS), but do you need JS for it? I think the following may do it:

list from ""
where contains(Name, "John")
1 Like

Thanks for the recommendation @atiz .
Yes I would like to use JS as I want to do the grouping with another fields into different sections with headings using let group of.

I have also tried the dataview groupby with the row.Name but it can only be in the same section.

1 Like

This script is fantastic, but it did have some trouble if there were multiple tables next to each other. For anyone else with that problem, here is the small edit to fix that.

 // Source https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript
        const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;

        const comparer = (idx, asc) => (a, b) => ((v1, v2) =>
            v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, undefined, { numeric: true })
        )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));

        document.querySelectorAll('table').forEach(table => {

            // do the work...
            Array.from(table.querySelectorAll('th')).forEach(th => th.style.cursor = "pointer");
            
            table.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
                const tbody = table.querySelector('tbody');
                Array.from(tbody.querySelectorAll('tr'))
                    .sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
                    .forEach(tr => tbody.appendChild(tr));
            })));
        })

wow, this is really impressive!
I’ve also seen your other posts about the inline countdown feature

<%+* let edate = moment(“12-24”, “MM-DD”); let from = moment().startOf(‘day’); edate.diff(from, “days”) >= 0 ? tR += edate.diff(from, “days”) : tR += edate.add(1, “year”).diff(from, “days”) %>

Can this be also included in this table?

Dear Moonbase,

I edited your code to allow an extra column of countdowns to the next birthday

var start = moment().startOf('day');
var end = moment(start).add(dv.current().duration);
var dateformat = "YYYY-MM-DD";
if (dv.current().dateformat) { dateformat = dv.current().dateformat; }

// info text above table, {0}=duration, {1}=start date, {2}=end date
// parameters can be left out, or the string empty
var infotext = "Upcoming birthdays for {0} from now ({1} – {2})<br><br>";

//======================================================================

function nextBirthday(birthday) {
    // Get person’s next birthday on or after "start"
    // returns a moment
    
    // need to "unparse" because DV has already converted YAML birthday to DateTime object
    // shouldn’t harm if already a string
    var bday = moment(birthday.toString());
    var bdayNext = moment(bday).year(start.year());
    if (bdayNext.isBefore(start, 'day')) {
        bdayNext.add(1, "year");
    }
    return bdayNext;
}

function turns(birthday) {
    // Get the age in years a person will turn to on their next birthday

    // need to "unparse" because DV has already converted YAML birthday to DateTime object
    // shouldn’t harm if already a string
    var bday = moment(birthday.toString());
    return nextBirthday(birthday).diff(bday, 'years');
}

function countdown(birthday){
	var bday = moment(birthday.toString())
	const setTime = new Date(bday);
	const nowTime = new Date();
	const restSec = setTime.getTime() - nowTime.getTime();
	const day = parseInt(restSec / (60*60*24*1000));

	const str = day + "天";
	
	return str;
	
}

function showBirthday(birthday) {
    // Determine if this birthday is in the range to be shown
    // including the start date, excluding the end date
    // because that comes from a duration calculation
    // for use with "where", returns true or false
    
    if (birthday) {
        // need to "unparse" because DV has already converted YAML birthday to DateTime object
        // shouldn’t harm if already a string
        var bday = moment(birthday.toString());
        var bdayNext = nextBirthday(birthday);
        if (bdayNext.isBetween(start, end, 'day', '[)')) {
            return true;
        } else {
            return false;
        }
    } else {
        return false;
    }
}

function sortByNextBirthday(a, b) {
    // comparator function for "sort"
    
  if (nextBirthday(a).isBefore(nextBirthday(b))) {
    return -1;
  }
  if (nextBirthday(a).isAfter(nextBirthday(b))) {
    return 1;
  }
  // they’re equal
  return 0;
}


//======================================================================

dv.paragraph(infotext.format(moment.duration(dv.current().duration.toString()).humanize(), start.format(dateformat), end.format(dateformat)));

dv.table(
    ["姓名", "生日/捡到日", "年龄(虚岁)","倒计时"],
    dv.pages(dv.current().searchterm)
        // use a function to see if this birthday is in range to be shown
        .where(p => showBirthday(p.birthday))
        // use a comparator function to sort by next birthday
        .sort(p => p.birthday, 'asc', sortByNextBirthday)
        .map(p => [
            p.file.link,
            p.birthday ? nextBirthday(p.birthday).format(dateformat) : '–',
            turns(p.birthday),
			countdown(nextBirthday(p.birthday)),
        ])
);
3 Likes

this was a good workaround for the bug with WHERE no longer working as intended. thank you!

1 Like

I’ve been utilizing my daily notes to track my habits as a list under a “Habits” header. I wrote some dataviewjs to help me keep up to date on these habits in a weekly roll-up in such a way that I am not required to define the habits in dataview.
Screenshot 2021-10-17 213102

This in combination with the Templater and Periodic Notes plugins let me automatically track habits in Obsidian with ease. Hopefully this can inspire other uses for dataviewjs.

var start = moment("2021-10-17");
var end = moment("2021-10-24");
var dailies = '"Daily Notes"';
var daily_fmt = 'YY-MM-DD';

let days = dv.pages(dailies).file
         .where(f => moment(f.name, daily_fmt).isBetween(start, end, 'day', '[]'))
         .sort(f => f.day);

let keys = ["Date"]
let habits = {}
days.forEach(day => {
    habits[day.name] = {}
    day.tasks.filter(task => task.link.subpath == "Habits").forEach(habit => {
        if (keys.indexOf(habit.text.toLowerCase()) == -1) {
            keys.push(habit.text.toLowerCase())
        }
        habits[day.name][habit.text.toLowerCase()] = habit.completed
    })
})

dv.table(keys.map(k => k[0].toUpperCase() + k.slice(1)), Object.keys(habits).map(k => {
    return [dv.fileLink(k)].concat(keys.slice(1).map(i => habits[k][i] ? "✔" : ""))
}))

Also, I decode the timestamp from the file name as I’ve found relying on the system for accurate timestamps for creation and modification is unreliable with my syncing solution. ymmv

9 Likes

Here is my costumizable task overview for Obsidian with DataviewJS. In the metadata various parameters can be changed and so the appearance of the overview can be customized. Also the stored regex parameters for the recognition of date, time and archived tasks can be customized as desired. This template is currently designed for use with the Kanban plugin for Obsidian, but can also be adapted for all other task management methods as needed.

702573n/Task-Overview-for-Obsidian-DataviewJS (github.com)

Like a Kanban, each task is displayed as a card and can be customized via metadata. Checkboxes, separated metadata like date, relative date, time, tags, cleaned up task texts, subpathes, as well as color markers can be set as desired. All tasks can be displayed in the categories Overdue, Today, Upcoming, Undated, Completed and Archived. Which categories are displayed is up to you.

---
#####################################
# TASK OVERVIEW - v1.0.0 BY 702573n #
#####################################
cssclass: KanbanOverview

# --- REGEX -------------
timeRegex: '@@{(\d{2}\:\d{2})}'
dateRegex: '@{(\d{4}\-\d{2}\-\d{2})}'
archiveRegex: 'archive{(\d{4}\-\d{2}\-\d{2})}'

# --- LAYOUT ------------
pages: '"Task Management/Kanbans"'
checkboxes: false
cleanedText: true
date_time: true
relativeDate: true
tags: true
subpath: false
subpathOnly: false

# --- SECTIONS ----------
overdue: true
today: true
upcoming: true
undated: true
completed: true
archived: false

# --- COLORS ------------
overviewColors: true
kanbanColors: true
borderPosition: left
borderThickness: 5
overdueColor: red
todayColor: yellow
upcomingColor: purple
undatedColor: 
completedColor: green
archivedColor: 
---
5 Likes

This is incredible and what I was looking for. Thank you!

I’ve tried to update your code for my YAML format of:

dates:
  birthday: 2000-01-01
  anniversary: 2000-01-01

But for some reason as soon as I change .where(p => showBirthday(p.birthday)) to .where(p => showBirthday(p.dates.birthday)) it completely bugs out. When I update the .sort and .map functions using p.dates.birthday, they work fine. It’s just the where() function.

Do you have any idea what I am missing? Or can it not be done? Thanks again

List of non-existing pages

The following snippet shows a list of all non-existing pages in your vault, i.e. topics which are mentioned [[using wikilinks]], but for which there is no destination.

Each item in the generated list is a link, that creates the note if you click it.

# List of non-existing pages
```dataviewjs
let r = Object.entries(dv.app.metadataCache.unresolvedLinks)
		.filter(([k,v])=>Object.keys(v).length)
		.flatMap(([k,v]) => Object.keys(v).map(x=>dv.fileLink(x)))
dv.list([...new Set(r)])
```

Note: I’m rather new to Obsidian (only one week), its api, and the dataview plugin and I don’t know if there is a simpler way to do this. Critizism is welcome!

4 Likes

Hello,

Last week I shared my amateur task management set up with notes as tasks.

I asked whether a clickable Done button could update the Done field, and thus remove the task from the list.

@asauceda94 helpfully suggested this could be done using MetaEdit and Dataview JS. I was able to get partway to the conversion. But I wonder if any JS experts could help finish the job, thanks in advance.

convert from standard dataview table

TABLE WITHOUT ID 
	file.link as File_________________________________, 
	do-date as "Do_Date", 
	Priority as Priority________, 
	due-date as "Due_Date", 
	recur-length as "Recur Length",
	do-date + default(recur-length, "") as "Next_Date",
	project as Project______,
FROM #✅/S/1_Active  and !"⚙️ System"
WHERE 	done = null,
		do-date < date(tomorrow)
SORT priority asc, do-date asc, file.mtime desc

to DataviewJS table

const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
    const btn = this.container.createEl('button', {"text": "Done"});
    const file = this.app.vault.getAbstractFileByPath(fpath)
    btn.addEventListener('click', async (evt) => {
        evt.preventDefault();
        await update(pn, pv, file);
    });
    return btn;
}
dv.table([
		"Done", 
		"File_________________________________", 
		"Do_Date", 
		"Priority", 
		"Due_Date", 
		"Recur Length",
		"Next_Date",
		"Project______",
		], 
	dv.pages("#✅/S/1_Active")
    .sort(t => t["priority"], 'asc')
	.sort(t => t["do-date"], 'asc')
    .where(t => !t.done)
    .map(t => 
		[buttonMaker('Done', 'Done', t.file.path),
		t.file.link, 
		t["do-date"],
		t.priority,
		t["due-date"],
		t["recur-length"],
		t["NEXT DATE QUERY"],
		t["project"],
    ])
    )
  • Remaining tasks in converting to JS
    1. WHERE command - done = null (where done field is empty) ==> ==Solved== .where(t => !t.done)
    2. WHERE command - WHERE do-date < date(tomorrow) (where do-date is today or before)
    3. FROM command - Exclude files from System Folder
    4. Next Date table query - do-date + default(recur-length, "") as "Next_Date",
    5. JS script - Make the ButtonMaker add the current date - possibly using this script await update('completed-date', DateTime.local().toISODate(), file);

Wonderful! Thank you!

it’s lot to ask for, but could you explain a little what happens in the code for us noobs in js?

I want to change the headers, both the names and how many. But little success so far.

Hi again @dryice !

Please note that I am NOT an expert in JS. These are what have worked for me, but I would not be surprised if there was a more optimal way to do this. However, I am here to help how I can. ALSO, one note about my DataviewJS style - I prefer to create a DV DataArray outside of a dv.table() call - I find that it keeps it a tad cleaner, and I am able to add more functionality that way.

  1. Comparing dates can be a little tricky from my experience. If you are using the standard ISO format in your DV fields (e.g. 2021-11-15) as I am:
// Below line of code is outside of the dv.pages() call
let today = DateTime.local().toISODate();

// I like to create my DV DataArrays outside of the dv.table call
let taskList = dv.pages("#tasks")
	.filter(t => t["due-date"] !== null)
	.filter(t => dv.date(t["due-date"]).toISODate() <= today);
  1. To my knowledge, I don’t think you can have two different DV sources in the FROM command. To solve this use case, I just add another filter statement to remove in the pages query:
let projectList = dv.pages("#projects")
	.filter(p => !p.file.folder.contains("INSERT FOLDER NAME HERE]"));
  1. For Next Date, I don’t have anything with this specific use case in my vault, but would probably involve using Luxon Duration. Please note I haven’t actually tested the implementation below, this is just a quick idea I cobbled together.
// All of your code above
.map(t =>
// More Code - rough NEXT DATE QUERY below
t["recur-length"] ? dv.date(t["do-date"]).plus({ days:  t["recur-length"] }).toISODate() : " ",
// Rest of your code
  1. My full button code (should have probs included it in other thread):
const {update} = this.app.plugins.plugins["metaedit"].api;
const buttonMaker = (pn, pv, fpath) => {
    const btn = this.container.createEl('button', {"text": "Finished!"});
    const file = this.app.vault.getAbstractFileByPath(fpath)
    btn.addEventListener('click', async (evt) => {
        evt.preventDefault();
        await update(pn, pv, file);
	await update('completed-date',DateTime.local().toISODate(),file);
    });
    return btn;
}

My friend @asauceda94 - noted… you’re not an expert, but you’ve definitely got skills and generosity - many thanks for your reply!

My progress: I have played around with your JS snippets, I have started a coding course to help, and I have tried really, really hard haha :sweat_smile:… But I feel like a toddler playing with a 1000 piece jigsaw puzzle.

Maybe I’ll have the skills someday in the future. It might be the instructional to see your code as a complete block, rather than walking me through step by step. What do you think?

But specifically I am having trouble with

  1. Insert Date Button code - I don’t know what goes into .map(t => at buttonMaker('FIELD', 'INSERTED TEXT', t.file.path)
  2. despite your explanations about DataviewJS style and arrays being outside of dv.table() call, I can’t find the right place for the “let” functions and filters. When I put them

Thanks!

Considering that daily notes name has the format 2021-11-23, the following snippet lists tasks from previous daily notes and future 3 days, excluding the current day.

let pages = dv.pages('"Daily note"');

dv.taskList(
	pages
	.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
	.sort(p => p.file.name, 'asc')
	.file
	.tasks
	.where(t => !t.completed))

I created three sets of task queries for using in Daily/Weekly/Monthly Notes. In the following examples, Daily Note, Weekly Note and Month Note have the formats 2021-01-01, 2021-W1 and 2021-11, respectively, and they can be managed by using the Periodic Notes plugin: liamcain/obsidian-periodic-notes: Create/manage your daily, weekly, and monthly notes in Obsidian.

For instance, the three sets of queries can be inserted into template Daily/Weekly/Monthly Notes in the following way:

  • In the Daily Note template, one can use the query for Daily, Weekly and Monthly Note tasks.
  • In the Weekly Note template, one can use the query for Weekly and Monthly Note tasks.
  • In the Monthly Note template, one can use the query for Monthly Note tasks.

Daily Note tasks:

==Due before and on = date({{date:YYYY-MM-DD}}), and Due in 3 days:==

let pages = dv.pages('"Daily note"');

dv.taskList(
	pages
	.where(p => (dv.date(p.file.name) <= dv.date(dv.current().file.name) + dv.duration('3 days')) && !dv.equal(dv.date(p.file.name),dv.date(dv.current().file.name)))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))

Weekly Note tasks:

==Due before and in Week = date({{date:YYYY-MM-DD}}).weekyear, and Due in Week = date({{date:YYYY-MM-DD}}).weekyear + 1:==

let pages = dv.pages('"Weekly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');

dv.taskList(
	pages
	.where(p => (p.file.name <= dateToday.year.toString() + '-W' + (dateToday.weekNumber + 1).toString()))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))

Monthly Note tasks:

==Due before and in Month = date({{date:YYYY-MM-DD}}).month, and Due in Month = date({{date:YYYY-MM-DD}}).month + 1:==

let pages = dv.pages('"Monthly note"');
let dateToday = dv.date('{{date:YYYY-MM-DD}}');

dv.taskList(
	pages
	.where(p => (p.file.name <= dateToday.year.toString() + '-' + (dateToday.month + 1).toString()))
	.sort(p => p.file.name, 'desc')
	.file
	.tasks
	.where(t => !t.completed))

Sure !
The first line is the header, like Paper, Title, Year etc…
All my files are in the folder Zettelkasten.

The line "[[" + b.file.name + "|" + (b.file.aliases[1] || b.file.aliases[0]) + "]]", means to create a link for the first column, you can skip it.
The lines

b.file.aliases[0],
b.Year,
b.completion_reading,
b.completion_note,

are the informations I have added in the header of the files, like this

image

Here is what my header looks like.