Tag based task aggregator

This one was really fun to build. As mentioned in my previously shared query (Advanced Query that pulls all reference AND recursive name spaces - #5 by CappieXL ), the way queries, namespaces, and aliases work together makes it hard for me to get it working to my liking. Therefore, I tried to go another route and instead of name spaces aggregate tasks based on page-tags.

I call this the “tag-based task aggregator”. Basically what it does, is that it collects all the tasks that are:

  1. on the current-page; or
  2. path-reference the current-page; or
  3. are on a page that has the current page as a tag; or
  4. path-reference a page that has the current page as a tag.

So imagine you’re working at “Bedrijf A”, which does a project “Project A.X” together with a client “Klant X”. If you’re like me, you jot everything down in the daily journal. So if my client calls me, I quickly make a task and I tag the project. Because the project has a page tag to both Bedrijf A and Klant X, the task will land on their task lists too.

Here are some screenshots:





To me this has two major advantages:

  1. I do not need to clutter my workflow with very long nested namespaces
  2. It becomes really intuitive. I can create project pages and just tag all the task lists i would like those tasks to show up.

Here’s the query:

#+BEGIN_QUERY
{:title "?currentpage taken"
:query [:find (pull ?b [*])
:in $ ?current-page
   :where
(task ?b #{"NOW" "LATER" "DOING" "TODO"})
(?p :block/name ?current-page)
(or
(and [?page :block/name ?page] [?b :block/path-refs ?p] )
(and (?page :block/tags ?p) [?b :block/path-refs ?page])
)
]
:result-transform (fn [result] (sort-by (fn [r] (get r :block/content)) result))
:group-by-page? false
:breadcrumb-show? false
:inputs [:current-page]
}
#+END_QUERY

It seems you use this, because the query throws an error.
However this is not the correct solution to that. Instead your or should be an or-join, like this.

(or-join [?p ?b]
  [?b :block/path-refs ?p]
  (and 
    [?page :block/tags ?p] 
    [?b :block/path-refs ?page]
  )
)

To explain, an or statement will be bound to the rest of the query.

“All clauses used in an or clause must use the same set of variables, which will unify with the surrounding query. This includes both the arguments to nested expression clauses as well as any bindings made by nested function expressions. Datomic will attempt to push the or clause down until all necessary variables are bound, and will throw an exception if that is not possible.”
From Datomic Queries and Rules | Datomic
(Although Logseq uses the datascript implementation and not datomic, they are similar.)

“An or-join is similar to an or clause, but it allows you to specify which variables should unify with the surrounding clause; only this list of variables needs binding before the clause can run. The variables specifies which variables should unify.”

In our case it is the variables ?p and ?b. So we write or-join [?p ?b]
The variable ?page is only relevant within the or-join.

Otherwise yes, tags and direct references are much easier to work with than namespaces :wink: kudos!

3 Likes

Oh wow, that is very interesting. And I thought I was so clever finding my neat little trick.

Thank you for taking the time for such an elaborate reply, I’ll change it to the or-join now I know how to use it properly!

3 Likes

This query works great, with @Siferiax 's suggested or-join. I’d love some help getting it to do a few additional things, if possible.

  1. Amend it so that it doesn’t use the current page, but some other input (for now, explicitly naming the tag. Later, perhaps the :parent-block content of the query).

  2. Check whether the input tag/page is one of a list of master task categories (e.g., “work”, “home”, “shopping”).

  3. Aggregate the tasks
    a. If tag is a valid master task category, aggregate the tasks as in the original query
    b. Otherwise, get all tasks that would not appear in the aggregated task queries of “work”, “home”, “shopping”

Item 1 is no problem.

Item 2 I think I can figure out by getting the child blocks of a “master task categories” page. Something like:

[?h :block/name "master task categories"]
[?i :block/content "Index"]
[?child :block/parent ?i]
[?child :block/refs ?categories] 

although I’m unclear on how to compare the input tag with the ?categories variable.

And Item 3b has got me stumped. I’ve tried all kinds of not and not-join combinations in place of the or-join, but I can’t get it working correctly.

Here’s the current code, which at the moment just covers item 1:

#+BEGIN_QUERY
; see https://discuss.logseq.com/t/tag-based-task-aggregator/21378/2
{:title "tasks related to current page"
  :query [:find (pull ?b [*])
    :in $ ?inputpage
      :where
      [task ?b #{"NOW" "LATER" "DOING" "TODO"}]
      [?p :block/name ?inputpage]
      (or-join [?p ?b]
        [?b :block/path-refs ?p]
        (and 
          [?page :block/tags ?p] 
          [?b :block/path-refs ?page]
        )
      )
  ]
  :result-transform (fn [result] (sort-by (fn [r] (get r :block/content)) result))
  :group-by-page? false
  :breadcrumb-show? false
  :inputs ["work"]
}
#+END_QUERY

Any suggestions would be appreciated!

I’m literally in a car on my phone, so can only give some pointers from memory.

For 2.
?categories are page id’s
So can get their name:
[?categories :block/name ?catname]
And then compare:
[(= ?catname ?inputpage)]
(Ps. I think you miss that ?i is on page ?h?)

For 3.
How should I read this?
How are tasks tagged? Do you mean that a tasks should be tagged with input, but not with a master category?

Thanks, that makes perfect sense. And yeah, not sure what I was thinking with ?h vs ?i!

Sorry, I wasn’t clear about that. Here’s what I’m after:

  • query input is a master category: query lists tasks with tags that reference the master category (current query does this)
  • query input is empty: query lists all tasks that dont reference any master category

The second part needs that code from Item 2 (check whether input is a master category) and it needs some kind of negative, recursive version of the or-join from the existing code. I think?

  • Should rather use a separate query for the special case of the remaining tasks.
  • If you insist on mixing them, try something like this:
    #+BEGIN_QUERY
    ; see https://discuss.logseq.com/t/tag-based-task-aggregator/21378/2
    {:title "tasks related to current page"
     :query [:find (pull ?b [*])
       :in $ ?inputpage
       :where
         [task ?b #{"NOW" "LATER" "DOING" "TODO"}]
         [(set ["work" "home" "shopping"]) ?categories]
         (or-join [?b ?inputpage ?categories]
           (and
             [(contains? ?categories ?inputpage)]
             [?p :block/name ?inputpage]
             (or-join [?p ?b]
               [?b :block/path-refs ?p]
               (and
                 [?page :block/tags ?p]
                 [?b :block/path-refs ?page]
               )
             )
           )
           (and
             (not [(contains? ?categories ?inputpage)])
             (not
               [?p :block/name ?pagename]
               [(contains? ?categories ?pagename)]
               (or-join [?p ?b]
                 [?b :block/path-refs ?p]
                 (and
                   [?page :block/tags ?p]
                   [?b :block/path-refs ?page]
                 )
               )
             )
           )
         )
     ]
     :result-transform (fn [result] (sort-by (fn [r] (get r :block/content)) result))
     :group-by-page? false
     :breadcrumb-show? false
     :inputs ["work"]
    }
    #+END_QUERY
    
    • For the special case, should still pass an input, just one not found in the list.
      • e.g. :inputs [""]
1 Like

Thank you for this! It works flawlessly. Very much appreciated, @mentaloid .

contains? is new to me (still just beginning to get acquainted with Datalog). Useful.

I can imagine that one reason for this might be unnecessary query length and complexity. It could be slightly helped perhaps by turning the shared block/path-refs sections into a rule. Are there any other reasons you suggest a separate query?

  • The problem is the complexity.
    • Maintaining complicated queries is …complicated.
  • Rules can reduce the length, but not the complexity.
1 Like

Makes sense, thank you :slight_smile:

@paulrudy I agree with this point. I have split many queries myself for this very reason.
And also added a lot of comments to my queries.

But it depends on your use case obviously.
Length isn’t necessarily an issue when it is readable.
Rather make something a little bit longer and more readable, than compact and cluttered.

1 Like

Makes sense. I suppose there’s nothing wrong with stacking a couple of simpler queries in separate blocks and reviewing both of them, if it means less maintenance