diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 2de30d70c..cfbce9419 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -16,17 +16,15 @@ [frontend.components.lazy-editor :as lazy-editor] [frontend.components.macro :as macro] [frontend.components.plugins :as plugins] - [frontend.components.query-table :as query-table] [frontend.components.query.builder :as query-builder-component] [frontend.components.svg :as svg] + [frontend.components.query :as query] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.date :as date] [frontend.db :as db] [frontend.db-mixins :as db-mixins] [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.latex :as latex] [frontend.extensions.lightbox :as lightbox] @@ -53,7 +51,6 @@ [frontend.handler.export.common :as export-common-handler] [frontend.mobile.util :as mobile-util] [frontend.modules.outliner.tree :as tree] - [frontend.search :as search] [frontend.security :as security] [frontend.state :as state] [frontend.template :as template] @@ -84,17 +81,7 @@ [datascript.impl.entity :as e] [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 (defonce *dragging? @@ -443,7 +430,7 @@ (defn image-link [config url href label metadata full_text] (let [metadata (if (string/blank? metadata) nil - (safe-read-string metadata false)) + (gp-util/safe-read-string metadata)) title (second (first label))] (ui/catch-error [:span.warning full_text] @@ -985,12 +972,10 @@ (not (= (:id config) "contents"))) [:span.text-gray-500 page-ref/right-brackets])])) -(declare custom-query) - (defn- show-link? [config metadata s full-text] (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])] (or (and @@ -1228,16 +1213,26 @@ (assoc :title title)) (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 (defn- macro-query-cp [config arguments] [:div.dsl-query.pr-3.sm:pr-0 (let [query (->> (string/join ", " arguments) (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 [config arguments] @@ -1544,9 +1539,13 @@ (defn hiccup->html [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 [{:keys [html-export?] :as config} item] @@ -2265,6 +2264,29 @@ [:a.fade-link 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 [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 @@ -2342,24 +2364,7 @@ (not= block-type :whiteboard-shape)) (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) :multiple-blocks @@ -3058,320 +3063,6 @@ [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 [config options html-export?] (when options @@ -3500,7 +3191,7 @@ ["Custom" "query" _options _result content] (try (let [query (reader/read-string content)] - (custom-query config query)) + (query/custom-query (wrap-query-components config) query)) (catch :default e (log/error :read-string-error e) (ui/block-error "Invalid query:" {:content content}))) diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index 71a2640aa..1af68942f 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -1,6 +1,7 @@ (ns frontend.components.page (:require [clojure.string :as string] [frontend.components.block :as component-block] + [frontend.components.query :as query] [frontend.components.content :as content] [frontend.components.editor :as editor] [frontend.components.hierarchy :as hierarchy] @@ -186,9 +187,11 @@ (rum/with-key (ui/catch-error (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))))])))) (defn tagged-pages diff --git a/src/main/frontend/components/query.cljs b/src/main/frontend/components/query.cljs new file mode 100644 index 000000000..b28b310b8 --- /dev/null +++ b/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}))) diff --git a/src/main/frontend/db/react.cljs b/src/main/frontend/db/react.cljs index 54619e959..9210107a1 100644 --- a/src/main/frontend/db/react.cljs +++ b/src/main/frontend/db/react.cljs @@ -144,7 +144,11 @@ (defn get-query-cached-result [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 [repo k {:keys [use-cache? transform-fn query-fn inputs-fn disable-reactive?] @@ -179,7 +183,7 @@ transform-fn)) result-atom (or result-atom (atom nil))] ;; 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? result-atom (add-q! k query time inputs result-atom transform-fn query-fn inputs-fn)))))))) diff --git a/src/main/frontend/format/mldoc.cljs b/src/main/frontend/format/mldoc.cljs index 95759b530..4a8eb7e54 100644 --- a/src/main/frontend/format/mldoc.cljs +++ b/src/main/frontend/format/mldoc.cljs @@ -7,7 +7,8 @@ [lambdaisland.glogi :as log] ["mldoc" :as mldoc :refer [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 parseOPML (gobj/get Mldoc "parseOPML")) @@ -79,3 +80,15 @@ [ast typ] (and (contains? #{"Drawer"} (ffirst 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)) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index e54bdff3a..b7deb70e6 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -3338,6 +3338,17 @@ (catch :default _e 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? ([block-id] (collapsable? block-id {})) @@ -3347,6 +3358,7 @@ (if-let [block (db-model/query-block-by-uuid block-id)] (or (db-model/has-children? block-id) (valid-dsl-query-block? block) + (valid-custom-query-block? block) (and (:outliner/block-title-collapse-enabled? (state/get-config)) (block-with-title? (:block/format block)