Mass delete query results

  • Preparation:
    • Add a macro inside file config.edn , inside macros{} :
    :deleteparent "<button class='kit eval' data-kit='massdelete'>► !!! DELETE !!! query results of parent block !!!</button>"
    
    • The code below requires having kits inside file custom.js .
    • Inside page MassDelete in Logseq, put the following code in a javascript code-block:
      const LS = logseq.api
      const Module = logseq.Module
      const Kits = Module.Kits
      const Msg = Module.Msg
      
      function advancedQuery(content, queryWord){
          const queryStart = content.indexOf("[", queryWord + 5)
      	if (queryStart < 0) return
      
          var queryEnd = queryStart + 1
          var n = 1
          while (n > 0) {
            const close = content.indexOf("]", queryEnd)
            if (close < 0) return
      
            const open = content.indexOf("[", queryEnd)
            if (close < open || open < 0) {
                queryEnd = close + 1
                n -= 1
                continue
            }
      
            queryEnd = open + 1
            n += 1
          }
      
          return content.slice(queryStart, queryEnd)
      }
      
      function simpleQuery(content, queryWord){
          const queryStart = queryWord + 6
          const queryEnd = content.indexOf("}}", queryStart)
          return content.slice(queryStart, queryEnd)
      }
      
      function getBlocks(content, queryWord){
      	if (queryWord < 0) return
      
          if (content[queryWord - 1] === ":") {
              const query = advancedQuery(content, queryWord)
              if (query) return LS.datascript_query(query).flat()
          } else if (content.slice(queryWord - 2, queryWord) === "{{") {
              const query = simpleQuery(content, queryWord)
              if (query) return LS.custom_query(query)
          }
      }
      
      logseq.kits.massdelete = Kits.addClickEventToButton.bind(null, function onDeleteParentClicked(e){
          const child = e.target.closest("div.ls-block")
          const parent = child.parentElement.closest("div.ls-block")
          const blockId = parent.getAttribute("blockid")
          const block = logseq.api.get_block(blockId)
          const content = block.content.replace(/\n?.*[:][:].*\n?/g, "\n").trim()
      
          const queryWord = content.indexOf("query")
          const res = getBlocks(content, queryWord)
      	if (!res) return Msg.ofStatus("Missing query", "warning")
      
          res.forEach( (r)=>{
              const name = r.name
              if (name) LS.delete_page(name)
              else LS.remove_block(r.uuid)
          })
          Msg.ofStatus("Deleted " + res.length, "success")
      })
      
  • Usage:
    • Create a query that returns the items for deletion:
      • either simple or advanced
      • either blocks or pages
      • keep it simple, as this is not tested with every possible query
    • Check the results to make sure that all of them are meant for deletion.
      • This code deletes information, so use with caution !
        • Ensure that you have a backup, just in case.
    • Put the macro at a child block under the query:
      • i.e. {{deleteparent}}
    • Press the button. This should automatically:
      • try reading the query from the parent block
        • the parser is basic, so:
          • avoid special strings
          • don’t use :inputs (read the following posts)
      • run the query again
      • delete without confirmation everything returned one-by-one
        • this is important, because it may be undoable
      • show a success message with the number of deleted items

Since updating a query to this format Switching a query’s scope between graph-level and page-level with a single argument, an instance of the button as a child under the query no longer works. A sample of the errors in the console:

Uncaught $APP.$cljs$core$ExceptionInfo$$ {message: 'Too few inputs passed, expected: [$ ?inputpage ?q], got: 1', data: $…P.$cljs$core$PersistentArrayMap$$, cause: null, name: 'Error', description: undefined, …}

If you have any ideas on how to work around this, I’d be grateful for your help!

  • I don’t understand that statement. What format do you talk about?
  • If a query fails, the button fails too.
  • Given the error message, I would try replacing the inputs with constants.

By format, I meant the type of query I posted in that thread (which you helped me with), which accepts :current-page as an argument—but in this case, the argument for using the current page is false, which means that the query returns global results, not current-page results.

Here’s the query in question:

#+BEGIN_QUERY
{:title [:h3 "☑️ DONE"]
 :query [:find (pull ?b [*])
         :in $ ?inputpage ?q
         :where
         [?b :block/page ?p]
         (or
          (not [(= ?q true)])
          (and [(= ?q true)]
               [?p :block/name ?inputpage]))
         [?b :block/marker ?marker]
         [(contains? #{"DONE"} ?marker)]]
 :result-transform (fn [result]
                     (sort-by (fn [h] (get h :block/priority "Z")) (map (fn [m] (assoc m :block/collapsed? true)) result)))
 :inputs [:current-page false] ; last argument (true|false): to query all pages, use false. to query current page only, use true.
 :table-view? false
 :breadcrumb-show? false
 :group-by-page? false
 :collapsed? false}
#+END_QUERY

The query doesn’t fail, only the button.

I can do that, but I’ll try to find another way around it so I can continue using the query with current inputs.

FYI, the previous version of the query (no inputs) works with the button:

#+BEGIN_QUERY
{:title [:h3 "☑️ DONE"]
 :query [:find (pull ?b [*])
         :where
         [?b :block/marker ?marker]
         [(contains? #{"DONE"} ?marker)]]
 :result-transform (fn [result]
                     (sort-by (fn [h] (get h :block/priority "Z")) (map (fn [m] (assoc m :block/collapsed? true)) result)))
 :table-view? false
 :breadcrumb-show? false
 :group-by-page? false
 :collapsed? false}
#+END_QUERY

Still would be cool to find a working solution for the query that uses inputs, however!

I reviewed the code. As stated in the description:

Among other things, it doesn’t parse the :inputs . Even if it was parsing them, it wouldn’t be able to replace special values like :current-page . Therefore, this code doesn’t support inputs at all. Since no inputs are read, no inputs are passed either, so the query complains as per the error message.

If you really want to mess with passing inputs, should:

  • go to function onDeleteParentClicked(e)
    • implement a way to store the desired inputs into variables
      • e.g. by reading them from e.target
      • This is the only hard part.
    • find getBlocks(content, queryWord) and pass the inputs as arguments
      • e.g. getBlocks(content, queryWord, ...inputs)
  • go to function getBlocks(content, queryWord)
    • add the extra parameters
      • e.g. function getBlocks(content, queryWord, ...inputs)
    • find LS.datascript_query(query) and pass the inputs as arguments
      • e.g. LS.datascript_query(query, ...inputs)

If the inputs are accepted and the query succeeds, the button will also succeed.

1 Like

Thank you so much for these pointers. Although my brain kind of wants to take on the challenge, I think I’d be just creating work for myself that isn’t really necessary.

I really appreciate you listing out how it could be done, and hope that someone finds it useful, if not me at some future time. It’s amazing what you’ve done with Kits.

P.S., appreciate you editing my incorrect link in the previous post for clarity

1 Like