Просмотр исходного кода

enhance: show custom title instead of live query for custom queries (#9026)

* enhance(ui): replace live query with custom title if exists

also moved query related components to frontend.components.query

* fix: simplify query components' state
Tienson Qin 2 лет назад
Родитель
Сommit
b17c4ea37e

+ 50 - 359
src/main/frontend/components/block.cljs

@@ -16,17 +16,15 @@
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.components.macro :as macro]
             [frontend.components.macro :as macro]
             [frontend.components.plugins :as plugins]
             [frontend.components.plugins :as plugins]
-            [frontend.components.query-table :as query-table]
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
+            [frontend.components.query :as query]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
-            [frontend.db.query-dsl :as query-dsl]
-            [frontend.db.utils :as db-utils]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.highlight :as highlight]
             [frontend.extensions.latex :as latex]
             [frontend.extensions.latex :as latex]
             [frontend.extensions.lightbox :as lightbox]
             [frontend.extensions.lightbox :as lightbox]
@@ -53,7 +51,6 @@
             [frontend.handler.export.common :as export-common-handler]
             [frontend.handler.export.common :as export-common-handler]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.tree :as tree]
-            [frontend.search :as search]
             [frontend.security :as security]
             [frontend.security :as security]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.template :as template]
             [frontend.template :as template]
@@ -84,17 +81,7 @@
             [datascript.impl.entity :as e]
             [datascript.impl.entity :as e]
             [logseq.common.path :as path]))
             [logseq.common.path :as path]))
 
 
-(defn safe-read-string
-  ([s]
-   (safe-read-string s true))
-  ([s warn?]
-   (try
-     (reader/read-string s)
-     (catch :default e
-       (log/error :read-string-error e :string s)
-       (when warn?
-         [:div.warning {:title "read-string failed"}
-          s])))))
+
 
 
 ;; local state
 ;; local state
 (defonce *dragging?
 (defonce *dragging?
@@ -443,7 +430,7 @@
 (defn image-link [config url href label metadata full_text]
 (defn image-link [config url href label metadata full_text]
   (let [metadata (if (string/blank? metadata)
   (let [metadata (if (string/blank? metadata)
                    nil
                    nil
-                   (safe-read-string metadata false))
+                   (gp-util/safe-read-string metadata))
         title (second (first label))]
         title (second (first label))]
     (ui/catch-error
     (ui/catch-error
      [:span.warning full_text]
      [:span.warning full_text]
@@ -985,12 +972,10 @@
                 (not (= (:id config) "contents")))
                 (not (= (:id config) "contents")))
        [:span.text-gray-500 page-ref/right-brackets])]))
        [:span.text-gray-500 page-ref/right-brackets])]))
 
 
-(declare custom-query)
-
 (defn- show-link?
 (defn- show-link?
   [config metadata s full-text]
   [config metadata s full-text]
   (let [media-formats (set (map name config/media-formats))
   (let [media-formats (set (map name config/media-formats))
-        metadata-show (:show (safe-read-string metadata))
+        metadata-show (:show (gp-util/safe-read-string metadata))
         format (get-in config [:block :block/format])]
         format (get-in config [:block :block/format])]
     (or
     (or
      (and
      (and
@@ -1228,16 +1213,26 @@
              (assoc :title title))
              (assoc :title title))
            (map-inline config label)))))))
            (map-inline config label)))))))
 
 
+(declare ->hiccup)
+
+(defn wrap-query-components
+  [config]
+  (merge config
+         {:->hiccup ->hiccup
+          :->elem ->elem
+          :page-cp page-cp
+          :inline-text inline-text
+          :map-inline map-inline}))
+
 ;;;; Macro component render functions
 ;;;; Macro component render functions
 (defn- macro-query-cp
 (defn- macro-query-cp
   [config arguments]
   [config arguments]
   [:div.dsl-query.pr-3.sm:pr-0
   [:div.dsl-query.pr-3.sm:pr-0
    (let [query (->> (string/join ", " arguments)
    (let [query (->> (string/join ", " arguments)
                     (string/trim))]
                     (string/trim))]
-     (custom-query (assoc config :dsl-query? true)
-                   {:title (rum/with-key (query-builder-component/builder query config)
-                             query)
-                    :query query}))])
+     (query/custom-query (wrap-query-components (assoc config :dsl-query? true))
+                         {:builder (query-builder-component/builder query config)
+                          :query query}))])
 
 
 (defn- macro-function-cp
 (defn- macro-function-cp
   [config arguments]
   [config arguments]
@@ -1544,9 +1539,13 @@
 
 
 (defn hiccup->html
 (defn hiccup->html
   [s]
   [s]
-  (-> (safe-read-string s)
-      (hiccups.core/html)
-      (security/sanitize-html)))
+  (let [result (gp-util/safe-read-string s)
+        result' (if (seq result) result
+                    [:div.warning {:title "Invalid hiccup"}
+                     s])]
+    (-> result'
+       (hiccups.core/html)
+       (security/sanitize-html))))
 
 
 (defn inline
 (defn inline
   [{:keys [html-export?] :as config} item]
   [{:keys [html-export?] :as config} item]
@@ -2265,6 +2264,29 @@
                     [:a.fade-link
                     [:a.fade-link
                      summary]])]))))
                      summary]])]))))
 
 
+(defn- block-content-inner
+  [config block body plugin-slotted? collapsed? block-ref-with-title?]
+  (if plugin-slotted?
+    [:div.block-slotted-body
+     (plugins/hook-block-slot
+      :block-content-slotted
+      (-> block (dissoc :block/children :block/page)))]
+
+    (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
+      (when (and (not block-ref-with-title?)
+                 (seq body)
+                 (or (not title-collapse-enabled?)
+                     (and title-collapse-enabled?
+                          (or (not collapsed?)
+                              (some? (mldoc/extract-first-query-from-ast body))))))
+        [:div.block-body
+         ;; TODO: consistent id instead of the idx (since it could be changed later)
+         (let [body (block/trim-break-lines! (:block/body block))]
+           (for [[idx child] (medley/indexed body)]
+             (when-let [block (markup-element-cp config child)]
+               (rum/with-key (block-child block)
+                 (str uuid "-" idx)))))]))))
+
 (rum/defc block-content < rum/reactive
 (rum/defc block-content < rum/reactive
   [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
   [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
   (let [{:block/keys [title body] :as block} (if (:block/title block) block
   (let [{:block/keys [title body] :as block} (if (:block/title block) block
@@ -2342,24 +2364,7 @@
                  (not= block-type :whiteboard-shape))
                  (not= block-type :whiteboard-shape))
         (properties-cp config block))
         (properties-cp config block))
 
 
-      (if plugin-slotted?
-        [:div.block-slotted-body
-         (plugins/hook-block-slot
-          :block-content-slotted
-          (-> block (dissoc :block/children :block/page)))]
-
-        (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
-          (when (and (not block-ref-with-title?)
-                     (seq body)
-                     (or (not title-collapse-enabled?)
-                         (and title-collapse-enabled? (not collapsed?))))
-            [:div.block-body
-             ;; TODO: consistent id instead of the idx (since it could be changed later)
-             (let [body (block/trim-break-lines! (:block/body block))]
-               (for [[idx child] (medley/indexed body)]
-                 (when-let [block (markup-element-cp config child)]
-                   (rum/with-key (block-child block)
-                                 (str uuid "-" idx)))))])))
+      (block-content-inner config block body plugin-slotted? collapsed? block-ref-with-title?)
 
 
       (case (:block/warning block)
       (case (:block/warning block)
         :multiple-blocks
         :multiple-blocks
@@ -3058,320 +3063,6 @@
   [config col]
   [config col]
   (map #(inline config %) col))
   (map #(inline config %) col))
 
 
-(declare ->hiccup)
-
-(defn built-in-custom-query?
-  [title]
-  (let [queries (get-in (state/sub-config) [:default-queries :journals])]
-    (when (seq queries)
-      (boolean (some #(= % title) (map :title queries))))))
-
-;; TODO: move query related fns/components to components.query
-(defn- trigger-custom-query!
-  [state *query-error *query-triggered?]
-  (let [[config query _query-result] (:rum/args state)
-        repo (state/get-current-repo)
-        result-atom (or (:query-atom state) (atom nil))
-        current-block-uuid (or (:block/uuid (:block config))
-                               (:block/uuid config))
-        _ (reset! *query-error nil)
-        query-atom (try
-                     (cond
-                       (:dsl-query? config)
-                       (let [q (:query query)
-                             form (safe-read-string q false)]
-                         (cond
-                           ;; Searches like 'foo' or 'foo bar' come back as symbols
-                           ;; and are meant to go directly to full text search
-                           (and (util/electron?) (symbol? form)) ; full-text search
-                           (p/let [blocks (search/block-search repo (string/trim (str form)) {:limit 30})]
-                             (when (seq blocks)
-                               (let [result (db/pull-many (state/get-current-repo) '[*] (map (fn [b] [:block/uuid (uuid (:block/uuid b))]) blocks))]
-                                 (reset! result-atom result))))
-
-                           (symbol? form)
-                           (atom nil)
-
-                           :else
-                           (query-dsl/query (state/get-current-repo) q)))
-
-                       :else
-                       (db/custom-query query {:current-block-uuid current-block-uuid}))
-                     (catch :default e
-                       (reset! *query-error e)
-                       (atom nil)))]
-    (when *query-triggered?
-      (reset! *query-triggered? true))
-    (if (instance? Atom query-atom)
-      query-atom
-      result-atom)))
-
-(rum/defc query-refresh-button
-  [query-time {:keys [on-mouse-down full-text-search?]}]
-  (ui/tippy
-   {:html  [:div
-            [:p
-             (if full-text-search?
-               [:span "Full-text search results will not be refreshed automatically."]
-               [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
-            [:p
-             "Click the refresh button instead if you want to see the latest result."]]
-    :interactive     true
-    :popperOptions   {:modifiers {:preventOverflow
-                                  {:enabled           true
-                                   :boundariesElement "viewport"}}}
-    :arrow true}
-   [:a.fade-link.flex
-    {:on-mouse-down on-mouse-down}
-    (ui/icon "refresh" {:style {:font-size 20}})]))
-
-(defn- get-query-result
-  [state config *query-error *query-triggered? current-block-uuid q not-grouped-by-page? query-result-atom]
-  (or (when-let [*result (:query-result config)] @*result)
-      (let [query-atom (trigger-custom-query! state *query-error *query-triggered?)
-            query-result (and query-atom (rum/react query-atom))
-            ;; exclude the current one, otherwise it'll loop forever
-            remove-blocks (if current-block-uuid [current-block-uuid] nil)
-            transformed-query-result (when query-result
-                                       (db/custom-query-result-transform query-result remove-blocks q))
-            result (if (and (:block/uuid (first transformed-query-result)) (not not-grouped-by-page?))
-                     (let [result (db-utils/group-by-page transformed-query-result)]
-                       (if (map? result)
-                         (dissoc result nil)
-                         result))
-                     transformed-query-result)]
-        (when query-result-atom
-          (reset! query-result-atom (util/safe-with-meta result (meta @query-atom))))
-        (when-let [query-result (:query-result config)]
-          (let [result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)]
-            (reset! query-result result)))
-        result)))
-
-(rum/defcs custom-query-inner < rum/reactive db-mixins/query
-  [state config {:keys [query children? breadcrumb-show?] :as q}
-   {:keys [query-result-atom
-           query-error-atom
-           query-triggered-atom
-           current-block
-           current-block-uuid
-           table?
-           dsl-query?
-           page-list?
-           view-f]}]
-  (let [*query-error query-error-atom
-        *query-triggered? query-triggered-atom
-        not-grouped-by-page? (or table?
-                                 (boolean (:result-transform q))
-                                 (and (string? query) (string/includes? query "(by-page false)")))
-        result (get-query-result state config *query-error *query-triggered? current-block-uuid q not-grouped-by-page? query-result-atom)
-        only-blocks? (:block/uuid (first result))
-        blocks-grouped-by-page? (and (seq result)
-                                     (not not-grouped-by-page?)
-                                     (coll? (first result))
-                                     (:block/name (ffirst result))
-                                     (:block/uuid (first (second (first result))))
-                                     true)]
-    (if @*query-error
-      (do
-        (log/error :exception @*query-error)
-        [:div.warning.my-1 "Query failed: "
-         [:p (.-message @*query-error)]])
-      [:div.custom-query-results
-       (cond
-         (and (seq result) view-f)
-         (let [result (try
-                        (sci/call-fn view-f result)
-                        (catch :default error
-                          (log/error :custom-view-failed {:error error
-                                                          :result result})
-                          [:div "Custom view failed: "
-                           (str error)]))]
-           (util/hiccup-keywordize result))
-
-         page-list?
-         (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text)
-
-         table?
-         (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text)
-
-         (and (seq result) (or only-blocks? blocks-grouped-by-page?))
-         (->hiccup result (cond-> (assoc config
-                                         :custom-query? true
-                                         :dsl-query? dsl-query?
-                                         :query query
-                                         :breadcrumb-show? (if (some? breadcrumb-show?)
-                                                             breadcrumb-show?
-                                                             true)
-                                         :group-by-page? blocks-grouped-by-page?
-                                         :ref? true)
-                            children?
-                            (assoc :ref? true))
-                   {:style {:margin-top "0.25rem"
-                            :margin-left "0.25rem"}})
-
-         (seq result)
-         (let [result (->>
-                       (for [record result]
-                         (if (map? record)
-                           (str (util/pp-str record) "\n")
-                           record))
-                       (remove nil?))]
-           (when (seq result)
-             [:ul
-              (for [item result]
-                [:li (str item)])]))
-
-         (or (string/blank? query)
-             (= query "(and)"))
-         nil
-
-         :else
-         [:div.text-sm.mt-2.opacity-90 "No matched result"])])))
-
-(rum/defc query-title
-  [config title {:keys [result-count]}]
-  [:div.custom-query-title.flex.justify-between.w-full
-   [:span.title-text (cond
-                       (vector? title) title
-                       (string? title) (inline-text config
-                                                    (get-in config [:block :block/format] :markdown)
-                                                    title)
-                       :else title)]
-   (when result-count
-     [:span.opacity-60.text-sm.ml-2.results-count
-      (str result-count (if (> result-count 1) " results" " result"))])])
-
-(rum/defcs ^:large-vars/cleanup-todo custom-query* < rum/reactive
-  (rum/local nil ::query-result)
-  (rum/local false ::query-triggered?)
-  {:init (fn [state] (assoc state :query-error (atom nil)))}
-  [state config {:keys [title query view collapsed? table-view?] :as q}]
-  (let [*query-error (:query-error state)
-        *query-triggered? (::query-triggered? state)
-        built-in? (built-in-custom-query? title)
-        *query-result (::query-result state)
-        result (rum/react *query-result)
-        dsl-query? (:dsl-query? config)
-        current-block-uuid (or (:block/uuid (:block config))
-                               (:block/uuid config))
-        current-block (db/entity [:block/uuid current-block-uuid])
-        temp-collapsed? (state/sub-collapsed current-block-uuid)
-        collapsed?' (if (some? temp-collapsed?)
-                      temp-collapsed?
-                      (or
-                       collapsed?
-                       (:block/collapsed? current-block)))
-        table? (or table-view?
-                   (get-in current-block [:block/properties :query-table])
-                   (and (string? query) (string/ends-with? (string/trim query) "table")))
-        query-time (:query-time (meta @*query-result))
-        view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
-        view-f (and view-fn (sci/eval-string (pr-str view-fn)))
-        page-list? (and (seq result)
-                        (some? (:block/name (first result))))
-        dsl-page-query? (and dsl-query?
-                             (false? (:blocks? (query-dsl/parse-query query))))
-        full-text-search? (and dsl-query?
-                               (util/electron?)
-                               (symbol? (safe-read-string query false)))
-        opts {:query-result-atom *query-result
-              :query-error-atom *query-error
-              :query-triggered-atom *query-triggered?
-              :current-block current-block
-              :dsl-query? dsl-query?
-              :current-block-uuid current-block-uuid
-              :table? table?
-              :view-f view-f
-              :page-list? page-list?}]
-    (if (:custom-query? config)
-      [:code (if dsl-query?
-               (util/format "{{query %s}}" query)
-               "{{query hidden}}")]
-      (if-not @*query-triggered?
-        ;; trigger custom query
-        (custom-query-inner config q opts)
-        (when-not (and built-in? (empty? @*query-result))
-          [:div.custom-query (get config :attr {})
-           (when-not built-in?
-             [:div.th
-              [:div.flex.flex-1.flex-row
-               (ui/icon "search" {:size 14})
-               [:div.ml-1 (str "Live query" (when dsl-page-query? " for pages"))]]
-              (when (or (not dsl-query?) (not collapsed?'))
-                [:div.flex.flex-row.items-center.fade-in
-                 (when (> (count result) 0)
-                   [:span.results-count
-                    (let [result-count (if (and (not table?) (map? result))
-                                         (apply + (map (comp count val) result))
-                                         (count result))]
-                      (str result-count (if (> result-count 1) " results" " result")))])
-
-                 (when (and current-block (not view-f) (nil? table-view?) (not page-list?))
-                   (if table?
-                     [:a.flex.ml-1.fade-link {:title "Switch to list view"
-                                              :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
-                                                                                                   "query-table"
-                                                                                                   false))}
-                      (ui/icon "list" {:style {:font-size 20}})]
-                     [:a.flex.ml-1.fade-link {:title "Switch to table view"
-                                              :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
-                                                                                                   "query-table"
-                                                                                                   true))}
-                      (ui/icon "table" {:style {:font-size 20}})]))
-
-                 [:a.flex.ml-1.fade-link
-                  {:title "Setting properties"
-                   :on-click (fn []
-                               (let [all-keys (query-table/get-keys result page-list?)]
-                                 (state/pub-event! [:modal/set-query-properties current-block all-keys])))}
-                  (ui/icon "settings" {:style {:font-size 20}})]
-
-                 [:div.ml-1
-                  (when (or full-text-search?
-                            (and query-time (> query-time 50)))
-                    (query-refresh-button query-time {:full-text-search? full-text-search?
-                                                      :on-mouse-down (fn [e]
-                                                                       (util/stop e)
-                                                                       (trigger-custom-query! state *query-error *query-triggered?))}))]])])
-           (if (or built-in? (not dsl-query?))
-             [:div {:style {:margin-left 2}}
-              (ui/foldable
-               (query-title config title (when built-in? {:result-count (count result)}))
-               (fn []
-                 (custom-query-inner config q opts))
-               {:default-collapsed? collapsed?
-                :title-trigger? true})]
-             [:div.bd
-              (query-title config title {})
-              (when-not collapsed?'
-                (custom-query-inner config q opts))])])))))
-
-(rum/defc custom-query
-  [config q]
-  (ui/catch-error
-   (ui/block-error "Query Error:" {:content (:query q)})
-   (ui/lazy-visible
-    (fn [] (custom-query* config q))
-    {:debug-id q
-     :trigger-once? false})))
-
-;; TODO: move to mldoc
-;; (defn- convert-md-src-to-custom-block
-;;   [item]
-;;   (let [{:keys [language options lines] :as options} (second item)
-;;         lang (string/lower-case (or language ""))]
-;;     (cond
-;;       (= lang "quote")
-;;       (let [content (string/trim (string/join "\n" lines))]
-;;         ["Quote" (first (mldoc/->edn content (gp-mldoc/default-config :markdown)))])
-
-;;       (contains? #{"query" "note" "tip" "important" "caution" "warning" "pinned"} lang)
-;;       (let [content (string/trim (string/join "\n" lines))]
-;;         ["Custom" lang nil (first (mldoc/->edn content (gp-mldoc/default-config :markdown))) content])
-
-;;       :else
-;;       ["Src" options])))
-
 (rum/defc src-cp < rum/static
 (rum/defc src-cp < rum/static
   [config options html-export?]
   [config options html-export?]
   (when options
   (when options
@@ -3500,7 +3191,7 @@
       ["Custom" "query" _options _result content]
       ["Custom" "query" _options _result content]
       (try
       (try
         (let [query (reader/read-string content)]
         (let [query (reader/read-string content)]
-          (custom-query config query))
+          (query/custom-query (wrap-query-components config) query))
         (catch :default e
         (catch :default e
           (log/error :read-string-error e)
           (log/error :read-string-error e)
           (ui/block-error "Invalid query:" {:content content})))
           (ui/block-error "Invalid query:" {:content content})))

+ 6 - 3
src/main/frontend/components/page.cljs

@@ -1,6 +1,7 @@
 (ns frontend.components.page
 (ns frontend.components.page
   (:require [clojure.string :as string]
   (:require [clojure.string :as string]
             [frontend.components.block :as component-block]
             [frontend.components.block :as component-block]
+            [frontend.components.query :as query]
             [frontend.components.content :as content]
             [frontend.components.content :as content]
             [frontend.components.editor :as editor]
             [frontend.components.editor :as editor]
             [frontend.components.hierarchy :as hierarchy]
             [frontend.components.hierarchy :as hierarchy]
@@ -186,9 +187,11 @@
            (rum/with-key
            (rum/with-key
              (ui/catch-error
              (ui/catch-error
               (ui/component-error "Failed default query:" {:content (pr-str query)})
               (ui/component-error "Failed default query:" {:content (pr-str query)})
-              (component-block/custom-query {:attr {:class "mt-10"}
-                                             :editor-box editor/box
-                                             :page page} query))
+              (query/custom-query (component-block/wrap-query-components
+                                   {:attr {:class "mt-10"}
+                                    :editor-box editor/box
+                                    :page page})
+                                  query))
              (str repo "-custom-query-" (:query query))))]))))
              (str repo "-custom-query-" (:query query))))]))))
 
 
 (defn tagged-pages
 (defn tagged-pages

+ 307 - 0
src/main/frontend/components/query.cljs

@@ -0,0 +1,307 @@
+(ns frontend.components.query
+  (:require [rum.core :as rum]
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [frontend.state :as state]
+            [frontend.search :as search]
+            [frontend.db :as db]
+            [frontend.db-mixins :as db-mixins]
+            [clojure.string :as string]
+            [frontend.db.query-dsl :as query-dsl]
+            [frontend.components.query-table :as query-table]
+            [frontend.db.utils :as db-utils]
+            [lambdaisland.glogi :as log]
+            [frontend.extensions.sci :as sci]
+            [frontend.handler.editor :as editor-handler]
+            [logseq.graph-parser.util :as gp-util]
+            [promesa.core :as p]))
+
+(defn built-in-custom-query?
+  [title]
+  (let [queries (get-in (state/sub-config) [:default-queries :journals])]
+    (when (seq queries)
+      (boolean (some #(= % title) (map :title queries))))))
+
+(defn- trigger-custom-query!
+  [state *query-error *query-triggered?]
+  (let [[config query] (:rum/args state)
+        repo (state/get-current-repo)
+        result-atom (atom nil)
+        current-block-uuid (or (:block/uuid (:block config))
+                               (:block/uuid config))
+        _ (reset! *query-error nil)
+        query-atom (try
+                     (cond
+                       (:dsl-query? config)
+                       (let [q (:query query)
+                             form (gp-util/safe-read-string q)]
+                         (cond
+                           ;; Searches like 'foo' or 'foo bar' come back as symbols
+                           ;; and are meant to go directly to full text search
+                           (and (util/electron?) (symbol? form)) ; full-text search
+                           (p/let [blocks (search/block-search repo (string/trim (str form)) {:limit 30})]
+                             (when (seq blocks)
+                               (let [result (db/pull-many (state/get-current-repo) '[*] (map (fn [b] [:block/uuid (uuid (:block/uuid b))]) blocks))]
+                                 (reset! result-atom result))))
+
+                           (symbol? form)
+                           (atom nil)
+
+                           :else
+                           (query-dsl/query (state/get-current-repo) q)))
+
+                       :else
+                       (db/custom-query query {:current-block-uuid current-block-uuid}))
+                     (catch :default e
+                       (reset! *query-error e)
+                       (atom nil)))]
+    (when *query-triggered?
+      (reset! *query-triggered? true))
+    (if (instance? Atom query-atom)
+      query-atom
+      result-atom)))
+
+(rum/defc query-refresh-button
+  [query-time {:keys [on-mouse-down full-text-search?]}]
+  (ui/tippy
+   {:html  [:div
+            [:p
+             (if full-text-search?
+               [:span "Full-text search results will not be refreshed automatically."]
+               [:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])]
+            [:p
+             "Click the refresh button instead if you want to see the latest result."]]
+    :interactive     true
+    :popperOptions   {:modifiers {:preventOverflow
+                                  {:enabled           true
+                                   :boundariesElement "viewport"}}}
+    :arrow true}
+   [:a.fade-link.flex
+    {:on-mouse-down on-mouse-down}
+    (ui/icon "refresh" {:style {:font-size 20}})]))
+
+(defn- get-query-result
+  [state config *query-error *query-triggered? current-block-uuid q not-grouped-by-page? ]
+  (or (when-let [*result (:query-result config)] @*result)
+      (let [query-atom (trigger-custom-query! state *query-error *query-triggered?)
+            query-result (and query-atom (rum/react query-atom))
+            ;; exclude the current one, otherwise it'll loop forever
+            remove-blocks (if current-block-uuid [current-block-uuid] nil)
+            transformed-query-result (when query-result
+                                       (db/custom-query-result-transform query-result remove-blocks q))
+            result (if (and (:block/uuid (first transformed-query-result)) (not not-grouped-by-page?))
+                     (let [result (db-utils/group-by-page transformed-query-result)]
+                       (if (map? result)
+                         (dissoc result nil)
+                         result))
+                     transformed-query-result)]
+        (when query-atom
+          (util/safe-with-meta result (meta @query-atom))))))
+
+(rum/defcs custom-query-inner < rum/reactive db-mixins/query
+  [state config {:keys [query children? breadcrumb-show?]}
+   {:keys [query-error-atom
+           current-block
+           table?
+           dsl-query?
+           page-list?
+           view-f
+           result]}]
+  (let [{:keys [->hiccup ->elem inline-text page-cp map-inline]} config
+        *query-error query-error-atom
+        not-grouped-by-page? (or table?
+                                 (and (string? query) (string/includes? query "(by-page false)")))
+        only-blocks? (:block/uuid (first result))
+        blocks-grouped-by-page? (and (seq result)
+                                     (not not-grouped-by-page?)
+                                     (coll? (first result))
+                                     (:block/name (ffirst result))
+                                     (:block/uuid (first (second (first result))))
+                                     true)]
+    (if @*query-error
+      (do
+        (log/error :exception @*query-error)
+        [:div.warning.my-1 "Query failed: "
+         [:p (.-message @*query-error)]])
+      [:div.custom-query-results
+       (cond
+         (and (seq result) view-f)
+         (let [result (try
+                        (sci/call-fn view-f result)
+                        (catch :default error
+                          (log/error :custom-view-failed {:error error
+                                                          :result result})
+                          [:div "Custom view failed: "
+                           (str error)]))]
+           (util/hiccup-keywordize result))
+
+         page-list?
+         (query-table/result-table config current-block result {:page? true} map-inline page-cp ->elem inline-text)
+
+         table?
+         (query-table/result-table config current-block result {:page? false} map-inline page-cp ->elem inline-text)
+
+         (and (seq result) (or only-blocks? blocks-grouped-by-page?))
+         (->hiccup result
+                   (cond-> (assoc config
+                                  :custom-query? true
+                                  :dsl-query? dsl-query?
+                                  :query query
+                                  :breadcrumb-show? (if (some? breadcrumb-show?)
+                                                      breadcrumb-show?
+                                                      true)
+                                  :group-by-page? blocks-grouped-by-page?
+                                  :ref? true)
+                     children?
+                     (assoc :ref? true))
+                   {:style {:margin-top "0.25rem"
+                            :margin-left "0.25rem"}})
+
+         (seq result)
+         (let [result (->>
+                       (for [record result]
+                         (if (map? record)
+                           (str (util/pp-str record) "\n")
+                           record))
+                       (remove nil?))]
+           (when (seq result)
+             [:ul
+              (for [item result]
+                [:li (str item)])]))
+
+         (or (string/blank? query)
+             (= query "(and)"))
+         nil
+
+         :else
+         [:div.text-sm.mt-2.opacity-90 "No matched result"])])))
+
+(rum/defc query-title
+  [config title {:keys [result-count]}]
+  (let [inline-text (:inline-text config)]
+    [:div.custom-query-title.flex.justify-between.w-full
+     [:span.title-text (cond
+                         (vector? title) title
+                         (string? title) (inline-text config
+                                                      (get-in config [:block :block/format] :markdown)
+                                                      title)
+                         :else title)]
+     (when result-count
+       [:span.opacity-60.text-sm.ml-2.results-count
+        (str result-count (if (> result-count 1) " results" " result"))])]))
+
+(rum/defcs ^:large-vars/cleanup-todo custom-query* < rum/reactive rum/static
+  (rum/local nil ::query-result)
+  {:init (fn [state] (assoc state :query-error (atom nil)))}
+  [state config {:keys [title builder query view collapsed? table-view?] :as q} *query-triggered?]
+  (let [*query-error (:query-error state)
+        built-in? (built-in-custom-query? title)
+        dsl-query? (:dsl-query? config)
+        current-block-uuid (or (:block/uuid (:block config))
+                               (:block/uuid config))
+        current-block (db/entity [:block/uuid current-block-uuid])
+        temp-collapsed? (state/sub-collapsed current-block-uuid)
+        collapsed?' (if (some? temp-collapsed?)
+                      temp-collapsed?
+                      (or
+                       collapsed?
+                       (:block/collapsed? current-block)))
+        table? (or table-view?
+                   (get-in current-block [:block/properties :query-table])
+                   (and (string? query) (string/ends-with? (string/trim query) "table")))
+        view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
+        view-f (and view-fn (sci/eval-string (pr-str view-fn)))
+        dsl-page-query? (and dsl-query?
+                             (false? (:blocks? (query-dsl/parse-query query))))
+        full-text-search? (and dsl-query?
+                               (util/electron?)
+                               (symbol? (gp-util/safe-read-string query)))
+        not-grouped-by-page? (or table?
+                                 (and (string? query) (string/includes? query "(by-page false)")))
+        result (when-not collapsed?'
+                 (get-query-result state config *query-error *query-triggered? current-block-uuid q not-grouped-by-page?))
+        query-time (:query-time (meta result))
+        page-list? (and (seq result)
+                        (some? (:block/name (first result))))
+        opts {:query-error-atom *query-error
+              :current-block current-block
+              :dsl-query? dsl-query?
+              :table? table?
+              :view-f view-f
+              :page-list? page-list?
+              :result result}]
+    (if (:custom-query? config)
+      [:code (if dsl-query?
+               (util/format "{{query %s}}" query)
+               "{{query hidden}}")]
+      (when-not (and built-in? (empty? result))
+        [:div.custom-query (get config :attr {})
+         (when-not built-in?
+           [:div.th
+            (if dsl-query?
+              [:div.flex.flex-1.flex-row
+               (ui/icon "search" {:size 14})
+               [:div.ml-1 (str "Live query" (when dsl-page-query? " for pages"))]]
+              [:div {:style {:font-size "initial"}} title])
+
+            (when (or (not dsl-query?) (not collapsed?'))
+              [:div.flex.flex-row.items-center.fade-in
+               (when (> (count result) 0)
+                 [:span.results-count
+                  (let [result-count (if (and (not table?) (map? result))
+                                       (apply + (map (comp count val) result))
+                                       (count result))]
+                    (str result-count (if (> result-count 1) " results" " result")))])
+
+               (when (and current-block (not view-f) (nil? table-view?) (not page-list?))
+                 (if table?
+                   [:a.flex.ml-1.fade-link {:title "Switch to list view"
+                                            :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
+                                                                                                 "query-table"
+                                                                                                 false))}
+                    (ui/icon "list" {:style {:font-size 20}})]
+                   [:a.flex.ml-1.fade-link {:title "Switch to table view"
+                                            :on-click (fn [] (editor-handler/set-block-property! current-block-uuid
+                                                                                                 "query-table"
+                                                                                                 true))}
+                    (ui/icon "table" {:style {:font-size 20}})]))
+
+               [:a.flex.ml-1.fade-link
+                {:title "Setting properties"
+                 :on-click (fn []
+                             (let [all-keys (query-table/get-keys result page-list?)]
+                               (state/pub-event! [:modal/set-query-properties current-block all-keys])))}
+                (ui/icon "settings" {:style {:font-size 20}})]
+
+               [:div.ml-1
+                (when (or full-text-search?
+                          (and query-time (> query-time 50)))
+                  (query-refresh-button query-time {:full-text-search? full-text-search?
+                                                    :on-mouse-down (fn [e]
+                                                                     (util/stop e)
+                                                                     (trigger-custom-query! state *query-error *query-triggered?))}))]])])
+
+         (when dsl-query? builder)
+
+         (if built-in?
+           [:div {:style {:margin-left 2}}
+            (ui/foldable
+             (query-title config title {:result-count (count result)})
+             (fn []
+               (custom-query-inner config q opts))
+             {:default-collapsed? collapsed?
+              :title-trigger? true})]
+           [:div.bd
+            (when-not collapsed?'
+              (custom-query-inner config q opts))])]))))
+
+(rum/defcs custom-query < rum/static
+  (rum/local false ::query-triggered?)
+  [state config q]
+  (ui/catch-error
+   (ui/block-error "Query Error:" {:content (:query q)})
+   (ui/lazy-visible
+    (fn []
+      (custom-query* config q (::query-triggered? state)))
+    {:debug-id q
+     :trigger-once? false})))

+ 6 - 2
src/main/frontend/db/react.cljs

@@ -144,7 +144,11 @@
 
 
 (defn get-query-cached-result
 (defn get-query-cached-result
   [k]
   [k]
-  (:result (get @query-state k)))
+  (when-let [result (get @query-state k)]
+    (when (satisfies? IWithMeta @(:result result))
+      (set! (.-state (:result result))
+           (with-meta @(:result result) {:query-time (:query-time result)})))
+    (:result result)))
 
 
 (defn q
 (defn q
   [repo k {:keys [use-cache? transform-fn query-fn inputs-fn disable-reactive?]
   [repo k {:keys [use-cache? transform-fn query-fn inputs-fn disable-reactive?]
@@ -179,7 +183,7 @@
                                             transform-fn))
                                             transform-fn))
                 result-atom (or result-atom (atom nil))]
                 result-atom (or result-atom (atom nil))]
             ;; Don't notify watches now
             ;; Don't notify watches now
-            (set! (.-state result-atom) (util/safe-with-meta result {:query-time time}))
+            (set! (.-state result-atom) result)
             (if disable-reactive?
             (if disable-reactive?
               result-atom
               result-atom
               (add-q! k query time inputs result-atom transform-fn query-fn inputs-fn))))))))
               (add-q! k query time inputs result-atom transform-fn query-fn inputs-fn))))))))

+ 14 - 1
src/main/frontend/format/mldoc.cljs

@@ -7,7 +7,8 @@
             [lambdaisland.glogi :as log]
             [lambdaisland.glogi :as log]
             ["mldoc" :as mldoc :refer [Mldoc]]
             ["mldoc" :as mldoc :refer [Mldoc]]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.graph-parser.util :as gp-util]))
+            [logseq.graph-parser.util :as gp-util]
+            [clojure.walk :as walk]))
 
 
 (defonce anchorLink (gobj/get Mldoc "anchorLink"))
 (defonce anchorLink (gobj/get Mldoc "anchorLink"))
 (defonce parseOPML (gobj/get Mldoc "parseOPML"))
 (defonce parseOPML (gobj/get Mldoc "parseOPML"))
@@ -79,3 +80,15 @@
   [ast typ]
   [ast typ]
   (and (contains? #{"Drawer"} (ffirst ast))
   (and (contains? #{"Drawer"} (ffirst ast))
        (= typ (second (first ast)))))
        (= typ (second (first ast)))))
+
+(defn extract-first-query-from-ast [ast]
+  (let [*result (atom nil)]
+    (walk/postwalk
+     (fn [f]
+       (if (and (vector? f)
+                (= "Custom" (first f))
+                (= "query" (second f)))
+         (reset! *result (last f))
+         f))
+     ast)
+    @*result))

+ 12 - 0
src/main/frontend/handler/editor.cljs

@@ -3338,6 +3338,17 @@
                          (catch :default _e
                          (catch :default _e
                            nil)))))))))
                            nil)))))))))
 
 
+(defn- valid-custom-query-block?
+  "Whether block has a valid customl query."
+  [block]
+  (let [entity (db/entity (:db/id block))
+        content (:block/content entity)]
+    (when (and (string/includes? content "#+BEGIN_QUERY")
+               (string/includes? content "#+END_QUERY"))
+      (let [ast (mldoc/->edn (string/trim content) (gp-mldoc/default-config (or (:block/format entity) :markdown)))
+            q (mldoc/extract-first-query-from-ast ast)]
+        (some? (:query (gp-util/safe-read-string q)))))))
+
 (defn collapsable?
 (defn collapsable?
   ([block-id]
   ([block-id]
    (collapsable? block-id {}))
    (collapsable? block-id {}))
@@ -3347,6 +3358,7 @@
      (if-let [block (db-model/query-block-by-uuid block-id)]
      (if-let [block (db-model/query-block-by-uuid block-id)]
        (or (db-model/has-children? block-id)
        (or (db-model/has-children? block-id)
            (valid-dsl-query-block? block)
            (valid-dsl-query-block? block)
+           (valid-custom-query-block? block)
            (and
            (and
             (:outliner/block-title-collapse-enabled? (state/get-config))
             (:outliner/block-title-collapse-enabled? (state/get-config))
             (block-with-title? (:block/format block)
             (block-with-title? (:block/format block)