mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 14:14:55 +00:00
feat: simple query builder (#8774)
Simple query builder --------- Co-authored-by: Gabriel Horner <gabriel@logseq.com> Co-authored-by: charlie <xyhp915@qq.com>
This commit is contained in:
@@ -68,6 +68,7 @@
|
||||
frontend.handler.page page-handler
|
||||
frontend.handler.plugin plugin-handler
|
||||
frontend.handler.plugin-config plugin-config-handler
|
||||
frontend.handler.query.builder query-builder
|
||||
frontend.handler.repo repo-handler
|
||||
frontend.handler.repo-config repo-config-handler
|
||||
frontend.handler.route route-handler
|
||||
|
||||
14
deps/db/src/logseq/db/default.cljs
vendored
14
deps/db/src/logseq/db/default.cljs
vendored
@@ -1,9 +1,19 @@
|
||||
(ns logseq.db.default
|
||||
"Provides fns for seeding default data in a logseq db"
|
||||
(:require [clojure.string :as string]))
|
||||
(:require [clojure.string :as string]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(defonce built-in-markers
|
||||
["NOW" "LATER" "DOING" "DONE" "CANCELED" "CANCELLED" "IN-PROGRESS" "TODO" "WAIT" "WAITING"])
|
||||
|
||||
(defonce built-in-priorities
|
||||
["A" "B" "C"])
|
||||
|
||||
(defonce built-in-pages-names
|
||||
#{"NOW" "LATER" "DOING" "DONE" "CANCELED" "CANCELLED" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C" "Favorites" "Contents" "card"})
|
||||
(set/union
|
||||
(set built-in-markers)
|
||||
(set built-in-priorities)
|
||||
#{"Favorites" "Contents" "card"}))
|
||||
|
||||
(def built-in-pages
|
||||
(mapv (fn [p]
|
||||
|
||||
1
deps/db/src/logseq/db/schema.cljs
vendored
1
deps/db/src/logseq/db/schema.cljs
vendored
@@ -118,6 +118,7 @@
|
||||
:block/properties
|
||||
:block/properties-order
|
||||
:block/properties-text-values
|
||||
:block/macros
|
||||
:block/invalid-properties
|
||||
:block/created-at
|
||||
:block/updated-at
|
||||
|
||||
@@ -254,12 +254,14 @@
|
||||
(legacy-title-parsing file-name-body)))
|
||||
|
||||
(defn safe-read-string
|
||||
[content]
|
||||
(try
|
||||
(reader/read-string content)
|
||||
(catch :default e
|
||||
(log/error :parse/read-string-failed e)
|
||||
{})))
|
||||
([content]
|
||||
(safe-read-string {} content))
|
||||
([opts content]
|
||||
(try
|
||||
(reader/read-string opts content)
|
||||
(catch :default e
|
||||
(log/error :parse/read-string-failed e)
|
||||
{}))))
|
||||
|
||||
;; Copied from Medley
|
||||
;; https://github.com/weavejester/medley/blob/d1e00337cf6c0843fb6547aadf9ad78d981bfae5/src/medley/core.cljc#L22
|
||||
|
||||
@@ -915,3 +915,10 @@ html.is-mobile {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
.ls-grid-cols {
|
||||
@apply grid grid-flow-col auto-cols-max;
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
:page/make-private :page/make-public]
|
||||
"(t (name" [] ;; shortcuts related
|
||||
"(t (dh/decorate-namespace" [] ;; shortcuts related
|
||||
"(t prompt-key" [:select/default-prompt :select.graph/prompt]
|
||||
"(t prompt-key" [:select/default-prompt :select/default-select-multiple :select.graph/prompt]
|
||||
;; All args to ui/make-confirm-modal are not keywords
|
||||
"(t title" []
|
||||
"(t subtitle" [:asset/physical-delete]})
|
||||
|
||||
@@ -268,7 +268,8 @@
|
||||
|
||||
;; advanced
|
||||
|
||||
[["Query" [[:editor/input "{{query }}" {:backward-pos 2}]] query-doc]
|
||||
[["Query" [[:editor/input "{{query }}" {:backward-pos 2}]
|
||||
[:editor/exit]] query-doc]
|
||||
["Zotero" (zotero-steps) "Import Zotero journal article"]
|
||||
["Query table function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query table function"]
|
||||
["Calculator" [[:editor/input "```calc\n\n```" {:backward-pos 4}]
|
||||
@@ -667,6 +668,9 @@
|
||||
(when-let [input-file (gdom/getElement "upload-file")]
|
||||
(.click input-file)))
|
||||
|
||||
(defmethod handle-step :editor/exit [[_]]
|
||||
(state/clear-edit!))
|
||||
|
||||
(defmethod handle-step :default [[type & _args]]
|
||||
(prn "No handler for step: " type))
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[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.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
@@ -25,7 +26,6 @@
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.db.react :as react]
|
||||
[frontend.db.utils :as db-utils]
|
||||
[frontend.extensions.highlight :as highlight]
|
||||
[frontend.extensions.latex :as latex]
|
||||
@@ -529,14 +529,16 @@
|
||||
(state/get-left-sidebar-open?))
|
||||
(ui-handler/close-left-sidebar!)))
|
||||
|
||||
(rum/defc page-inner
|
||||
(rum/defcs page-inner <
|
||||
(rum/local false ::mouse-down?)
|
||||
"The inner div of page reference component
|
||||
|
||||
page-name-in-block is the overridable name of the page (legacy)
|
||||
|
||||
All page-names are sanitized except page-name-in-block"
|
||||
[config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label whiteboard-page?]
|
||||
(let [tag? (:tag? config)
|
||||
[state config page-name-in-block page-name redirect-page-name page-entity contents-page? children html-export? label whiteboard-page?]
|
||||
(let [*mouse-down? (::mouse-down? state)
|
||||
tag? (:tag? config)
|
||||
config (assoc config :whiteboard-page? whiteboard-page?)
|
||||
untitled? (model/untitled-page? page-name)]
|
||||
[:a
|
||||
@@ -548,7 +550,11 @@
|
||||
:data-ref page-name
|
||||
:draggable true
|
||||
:on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name e))
|
||||
:on-mouse-up (fn [e] (open-page-ref e page-name redirect-page-name page-name-in-block contents-page? whiteboard-page?))
|
||||
:on-mouse-down (fn [_e] (reset! *mouse-down? true))
|
||||
:on-mouse-up (fn [e]
|
||||
(when @*mouse-down?
|
||||
(open-page-ref e page-name redirect-page-name page-name-in-block contents-page? whiteboard-page?)
|
||||
(reset! *mouse-down? false)))
|
||||
:on-key-up (fn [e] (when (and e (= (.-key e) "Enter"))
|
||||
(open-page-ref e page-name redirect-page-name page-name-in-block contents-page? whiteboard-page?)))}
|
||||
|
||||
@@ -1216,14 +1222,10 @@
|
||||
[:div.dsl-query.pr-3.sm:pr-0
|
||||
(let [query (->> (string/join ", " arguments)
|
||||
(string/trim))]
|
||||
(when-not (string/blank? query)
|
||||
(custom-query (assoc config :dsl-query? true)
|
||||
{:title (ui/tippy {:html commands/query-doc
|
||||
:interactive true
|
||||
:in-editor? true}
|
||||
[:span.font-medium.px-2.py-1.query-title.text-sm.rounded-md.shadow-xs
|
||||
(str "Query: " query)])
|
||||
:query query})))])
|
||||
(custom-query (assoc config :dsl-query? true)
|
||||
{:title (rum/with-key (query-builder-component/builder query config)
|
||||
query)
|
||||
:query query}))])
|
||||
|
||||
(defn- macro-function-cp
|
||||
[config arguments]
|
||||
@@ -2151,11 +2153,12 @@
|
||||
(defn- block-content-on-mouse-down
|
||||
[e block block-id content edit-input-id]
|
||||
(when-not (> (count content) (state/block-content-max-length (state/get-current-repo)))
|
||||
(.stopPropagation e)
|
||||
(let [target (gobj/get e "target")
|
||||
button (gobj/get e "buttons")
|
||||
shift? (gobj/get e "shiftKey")
|
||||
meta? (util/meta-key? e)]
|
||||
meta? (util/meta-key? e)
|
||||
forbidden-edit? (target-forbidden-edit? target)]
|
||||
(when-not forbidden-edit? (.stopPropagation e))
|
||||
(if (and meta?
|
||||
(not (state/get-edit-input-id))
|
||||
(not (dom/has-class? target "page-ref"))
|
||||
@@ -2166,7 +2169,7 @@
|
||||
(when block-id
|
||||
(state/set-selection-start-block! block-id)))
|
||||
(when (contains? #{1 0} button)
|
||||
(when-not (target-forbidden-edit? target)
|
||||
(when-not forbidden-edit?
|
||||
(cond
|
||||
(and shift? (state/get-selection-start-block-or-first))
|
||||
(do
|
||||
@@ -2863,7 +2866,14 @@
|
||||
(select-keys b2 compare-keys))
|
||||
(not= (select-keys (first (:rum/args old-state)) config-compare-keys)
|
||||
(select-keys (first (:rum/args new-state)) config-compare-keys)))]
|
||||
(boolean result)))}
|
||||
(boolean result)))
|
||||
:will-unmount (fn [state]
|
||||
;; restore root block's collapsed state
|
||||
(let [[config block] (:rum/args state)
|
||||
block-id (:block/uuid block)]
|
||||
(when (root-block? config block)
|
||||
(state/set-collapsed-block! block-id nil)))
|
||||
state)}
|
||||
[state config block]
|
||||
(let [repo (state/get-current-repo)
|
||||
ref? (:ref? config)
|
||||
@@ -3035,53 +3045,50 @@
|
||||
(boolean (some #(= % title) (map :title queries))))))
|
||||
|
||||
(defn- trigger-custom-query!
|
||||
[state]
|
||||
(let [[config query] (:rum/args state)
|
||||
[state *query-error]
|
||||
(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))
|
||||
[full-text-search? query-atom] (if (: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
|
||||
[true
|
||||
(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))))]
|
||||
_ (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)
|
||||
[false (atom nil)]
|
||||
(symbol? form)
|
||||
(atom nil)
|
||||
|
||||
:else
|
||||
[false (query-dsl/query (state/get-current-repo) q)]))
|
||||
[false (db/custom-query query {:current-block-uuid current-block-uuid})])
|
||||
query-atom (if (instance? Atom query-atom)
|
||||
query-atom
|
||||
result-atom)]
|
||||
(assoc state
|
||||
:query-atom query-atom
|
||||
:full-text-search? full-text-search?)))
|
||||
:else
|
||||
(query-dsl/query (state/get-current-repo) q)))
|
||||
|
||||
(defn- clear-custom-query!
|
||||
[dsl? query]
|
||||
(let [query (if dsl? (:query query) query)]
|
||||
(state/remove-custom-query-component! query)
|
||||
(db/remove-custom-query! (state/get-current-repo) query)))
|
||||
:else
|
||||
(db/custom-query query {:current-block-uuid current-block-uuid}))
|
||||
(catch :default e
|
||||
(reset! *query-error e)
|
||||
(atom nil)))]
|
||||
(if (instance? Atom query-atom)
|
||||
query-atom
|
||||
result-atom)))
|
||||
|
||||
(rum/defc query-refresh-button
|
||||
[state query-time {:keys [on-mouse-down]}]
|
||||
[query-time {:keys [on-mouse-down full-text-search?]}]
|
||||
(ui/tippy
|
||||
{:html [:div
|
||||
[:p
|
||||
(when (and query-time (> query-time 80))
|
||||
[:span (str "This query takes " (int query-time) "ms to finish, it's a bit slow so that auto refresh is disabled.")])
|
||||
(when (:full-text-search? state)
|
||||
[:span "Full-text search results will not be refreshed automatically."])]
|
||||
(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
|
||||
@@ -3089,163 +3096,212 @@
|
||||
{:enabled true
|
||||
:boundariesElement "viewport"}}}
|
||||
:arrow true}
|
||||
[:a.control.fade-link.ml-1.inline-flex
|
||||
{:style {:margin-top 7}
|
||||
:on-mouse-down on-mouse-down}
|
||||
[:a.fade-link.flex
|
||||
{:on-mouse-down on-mouse-down}
|
||||
(ui/icon "refresh" {:style {:font-size 20}})]))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo custom-query* < rum/reactive
|
||||
{:will-mount trigger-custom-query!
|
||||
:did-mount (fn [state]
|
||||
(when-let [query (last (:rum/args state))]
|
||||
(state/add-custom-query-component! query (:rum/react-component state)))
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(when-let [query (last (:rum/args state))]
|
||||
(clear-custom-query! (:dsl-query? (first (:rum/args state)))
|
||||
query))
|
||||
state)}
|
||||
[state config {:keys [title query view collapsed? children? breadcrumb-show? table-view?] :as q}]
|
||||
(let [dsl-query? (:dsl-query? config)
|
||||
query-atom (:query-atom state)
|
||||
query-time (or (react/get-query-time query)
|
||||
(react/get-query-time q))
|
||||
view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
|
||||
current-block-uuid (or (:block/uuid (:block config))
|
||||
(:block/uuid config))
|
||||
current-block (db/entity [:block/uuid current-block-uuid])
|
||||
(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
|
||||
current-block
|
||||
current-block-uuid
|
||||
table?
|
||||
dsl-query?
|
||||
page-list?
|
||||
built-in-query?
|
||||
view-f]}]
|
||||
(let [*query-error query-error-atom
|
||||
query-atom (if built-in-query? query-result-atom (trigger-custom-query! state *query-error))
|
||||
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)
|
||||
query-result (and query-atom (rum/react query-atom))
|
||||
table? (or table-view?
|
||||
(get-in current-block [:block/properties :query-table])
|
||||
(and (string? query) (string/ends-with? (string/trim query) "table")))
|
||||
transformed-query-result (when query-result
|
||||
(db/custom-query-result-transform query-result remove-blocks q))
|
||||
not-grouped-by-page? (or table?
|
||||
(boolean (:result-transform q))
|
||||
(and (string? query) (string/includes? query "(by-page false)")))
|
||||
result (if (and (:block/uuid (first transformed-query-result)) (not not-grouped-by-page?))
|
||||
(db-utils/group-by-page transformed-query-result)
|
||||
(let [result (db-utils/group-by-page transformed-query-result)]
|
||||
(if (map? result)
|
||||
(dissoc result nil)
|
||||
result))
|
||||
transformed-query-result)
|
||||
_ (when (and query-result-atom (not built-in-query?))
|
||||
(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)))
|
||||
view-f (and view-fn (sci/eval-string (pr-str view-fn)))
|
||||
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)
|
||||
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]
|
||||
[: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)]])
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo custom-query* < rum/reactive
|
||||
(rum/local nil ::query-result)
|
||||
{: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)
|
||||
built-in? (built-in-custom-query? title)
|
||||
*query-result (if built-in?
|
||||
(trigger-custom-query! state *query-error)
|
||||
(::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)
|
||||
(:block/name (first result)))
|
||||
nested-query? (:custom-query? config)]
|
||||
(if nested-query?
|
||||
(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)))]
|
||||
(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.mt-4 (get config :attr {})
|
||||
(ui/foldable
|
||||
[:div.custom-query-title.flex.justify-between.w-full
|
||||
[:div.flex.items-center
|
||||
[:span.title-text (cond
|
||||
(vector? title) title
|
||||
(string? title) (inline-text config
|
||||
(get-in config [:block :block/format] :markdown)
|
||||
title)
|
||||
:else title)]
|
||||
[:span.opacity-60.text-sm.ml-2.results-count
|
||||
(str (count result) " results")]]
|
||||
(let [opts {:query-result-atom *query-result
|
||||
:query-error-atom *query-error
|
||||
:current-block current-block
|
||||
:dsl-query? dsl-query?
|
||||
:current-block-uuid current-block-uuid
|
||||
:table? table?
|
||||
:view-f view-f
|
||||
:page-list? page-list?
|
||||
:built-in-query? built-in?}]
|
||||
[: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-not collapsed?'
|
||||
[:div.flex.flex-row.items-center.fade-in
|
||||
(when (> (count result) 0)
|
||||
[:span.results-count
|
||||
(str (count result) (if (> (count result) 1) " results" " result"))])
|
||||
|
||||
;;insert an "edit" button in the query view
|
||||
[:div.flex.items-center
|
||||
(when-not built-in?
|
||||
[:a.opacity-70.hover:opacity-100.svg-small.inline
|
||||
{:on-mouse-down (fn [e]
|
||||
(util/stop e)
|
||||
(editor-handler/edit-block! current-block :max (:block/uuid current-block)))}
|
||||
svg/edit])
|
||||
(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}})]))
|
||||
|
||||
(when (or (:full-text-search? state)
|
||||
(and query-time (> query-time 80)))
|
||||
(query-refresh-button state query-time
|
||||
{:on-mouse-down (fn [e]
|
||||
(util/stop e)
|
||||
(trigger-custom-query! state))}))]]
|
||||
(fn []
|
||||
[:div
|
||||
(when (and current-block (not view-f) (nil? table-view?))
|
||||
[:div.flex.flex-row.align-items.mt-2 {:on-mouse-down (fn [e] (util/stop e))}
|
||||
(when-not page-list?
|
||||
[:div.flex.flex-row
|
||||
[:div.mx-2 [:span.text-sm "Table view"]]
|
||||
[:div {:style {:margin-top 5}}
|
||||
(ui/toggle table?
|
||||
(fn []
|
||||
(editor-handler/set-block-property! current-block-uuid
|
||||
"query-table"
|
||||
(not table?)))
|
||||
true)]])
|
||||
[: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}})]
|
||||
|
||||
[:a.mx-2.block.fade-link
|
||||
{:on-click (fn []
|
||||
(let [all-keys (query-table/get-keys result page-list?)]
|
||||
(state/pub-event! [:modal/set-query-properties current-block all-keys])))}
|
||||
[:span.table-query-properties
|
||||
[:span.text-sm.mr-1 "Set properties"]
|
||||
svg/settings-sm]]])
|
||||
(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?))]
|
||||
[:pre result])
|
||||
|
||||
:else
|
||||
[:div.text-sm.mt-2.ml-2.font-medium.opacity-50 "Empty"])])
|
||||
{:default-collapsed? collapsed?
|
||||
:title-trigger? true
|
||||
:on-mouse-down (fn [collapsed?]
|
||||
(when collapsed?
|
||||
(clear-custom-query! dsl-query? q)))})]))))
|
||||
[: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))}))]])])
|
||||
(if built-in?
|
||||
(ui/foldable
|
||||
(query-title config title)
|
||||
(fn []
|
||||
(custom-query-inner config q opts))
|
||||
{})
|
||||
[:div.bd
|
||||
(query-title config title)
|
||||
(when-not collapsed?'
|
||||
(custom-query-inner config q opts))])])))))
|
||||
|
||||
(rum/defc custom-query
|
||||
[config q]
|
||||
|
||||
@@ -538,6 +538,12 @@ a:hover > .bullet-container {
|
||||
}
|
||||
}
|
||||
|
||||
.ls-block .custom-query {
|
||||
> .th {
|
||||
@apply flex flex-row flex-1 items-center justify-between my-1 text-xs opacity-90;
|
||||
}
|
||||
}
|
||||
|
||||
/* copied from https://github.com/drdogbot7/tailwindcss-responsive-embed */
|
||||
.embed-responsive {
|
||||
position: relative;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{:label "w"}
|
||||
{:label "m"}
|
||||
{:label "y"}])
|
||||
(fn [value]
|
||||
(fn [_e value]
|
||||
(swap! *timestamp assoc-in [:repeater :duration] value))
|
||||
nil)
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@
|
||||
;; item))
|
||||
;; [{:label "gForce"}
|
||||
;; {:label "dagre"}])
|
||||
;; (fn [value]
|
||||
;; (fn [_e value]
|
||||
;; (set-setting! :layout value))
|
||||
;; "graph-layout")]
|
||||
[:div.flex.items-center.justify-between.mb-2
|
||||
|
||||
@@ -391,7 +391,8 @@
|
||||
{:label "Direct" :value "direct" :selected (= type "direct")}
|
||||
{:label "HTTP" :value "http" :selected (= type "http")}
|
||||
{:label "SOCKS5" :value "socks5" :selected (= type "socks5")}]
|
||||
#(set-opts! (assoc opts :type % :protocol %)))]]
|
||||
(fn [_e value]
|
||||
(set-opts! (assoc opts :type value :protocol value))))]]
|
||||
[:p.flex
|
||||
[:label.pr-4
|
||||
{:class (if disabled? "opacity-50" nil)}
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
:radio (ui/radio-list options #(update-setting! key %) nil)
|
||||
:checkbox (ui/checkbox-list options #(update-setting! key %) nil)
|
||||
;; select
|
||||
(ui/select options #(update-setting! key %) nil))
|
||||
(ui/select options (fn [_ value ] (update-setting! key value)) nil))
|
||||
]]]))
|
||||
|
||||
(rum/defc render-item-object
|
||||
|
||||
463
src/main/frontend/components/query/builder.cljs
Normal file
463
src/main/frontend/components/query/builder.cljs
Normal file
@@ -0,0 +1,463 @@
|
||||
(ns frontend.components.query.builder
|
||||
"DSL query builder."
|
||||
(:require [frontend.ui :as ui]
|
||||
[frontend.date :as date]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.model :as db-model]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.query.builder :as query-builder]
|
||||
[frontend.components.select :as component-select]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[frontend.search :as search]
|
||||
[frontend.mixins :as mixins]
|
||||
[logseq.db.default :as db-default]
|
||||
[rum.core :as rum]
|
||||
[clojure.string :as string]
|
||||
[logseq.graph-parser.util :as gp-util]
|
||||
[logseq.graph-parser.util.page-ref :as page-ref]))
|
||||
|
||||
(rum/defc page-block-selector
|
||||
[*find]
|
||||
[:div.filter-item {:on-mouse-down (fn [e] (util/stop-propagation e))}
|
||||
(ui/select [{:label "Blocks"
|
||||
:value "block"
|
||||
:selected (not= @*find :page)}
|
||||
{:label "Pages"
|
||||
:value "page"
|
||||
:selected (= @*find :page)}]
|
||||
(fn [e v]
|
||||
;; Prevent opening the current block's editor
|
||||
(util/stop e)
|
||||
(reset! *find (keyword v))))])
|
||||
|
||||
(defn- select
|
||||
([items on-chosen]
|
||||
(select items on-chosen {}))
|
||||
([items on-chosen options]
|
||||
(component-select/select (merge
|
||||
{:items items
|
||||
:on-chosen on-chosen
|
||||
:extract-fn nil}
|
||||
options))))
|
||||
|
||||
(defn append-tree!
|
||||
[*tree {:keys [toggle-fn toggle?]
|
||||
:or {toggle? true}} loc x]
|
||||
(swap! *tree #(query-builder/append-element % loc x))
|
||||
(when toggle? (toggle-fn)))
|
||||
|
||||
(rum/defcs search < (rum/local nil ::input-value)
|
||||
(mixins/event-mixin
|
||||
(fn [state]
|
||||
(mixins/on-key-down
|
||||
state
|
||||
{;; enter
|
||||
13 (fn [state e]
|
||||
(let [input-value (get state ::input-value)]
|
||||
(when-not (string/blank? @input-value)
|
||||
(util/stop e)
|
||||
(let [on-submit (first (:rum/args state))]
|
||||
(on-submit @input-value))
|
||||
(reset! input-value nil))))
|
||||
;; escape
|
||||
27 (fn [_state _e]
|
||||
(let [[_on-submit on-cancel] (:rum/args state)]
|
||||
(on-cancel)))})))
|
||||
[state _on-submit _on-cancel]
|
||||
(let [*input-value (::input-value state)]
|
||||
[:input#query-builder-search.form-input.block.sm:text-sm.sm:leading-5
|
||||
{:auto-focus true
|
||||
:placeholder "Full text search"
|
||||
:aria-label "Full text search"
|
||||
:on-change #(reset! *input-value (util/evalue %))}]))
|
||||
|
||||
(defonce *shown-datepicker (atom nil))
|
||||
(defonce *between-dates (atom {}))
|
||||
(rum/defcs datepicker < rum/reactive
|
||||
(rum/local nil ::input-value)
|
||||
{:init (fn [state]
|
||||
(when (:auto-focus (last (:rum/args state)))
|
||||
(reset! *shown-datepicker (first (:rum/args state))))
|
||||
state)
|
||||
:will-unmount (fn [state]
|
||||
(swap! *between-dates dissoc (first (:rum/args state)))
|
||||
state)}
|
||||
[state id placeholder {:keys [auto-focus]}]
|
||||
(let [*input-value (::input-value state)
|
||||
show? (= id (rum/react *shown-datepicker))]
|
||||
[:div.ml-4
|
||||
[:input.query-builder-datepicker.form-input.block.sm:text-sm.sm:leading-5
|
||||
{:auto-focus (or auto-focus false)
|
||||
:placeholder placeholder
|
||||
:aria-label placeholder
|
||||
:value @*input-value
|
||||
:on-click #(reset! *shown-datepicker id)}]
|
||||
(when show?
|
||||
(ui/datepicker nil {:on-change (fn [_e date]
|
||||
(let [journal-date (date/journal-name date)]
|
||||
(reset! *input-value journal-date)
|
||||
(reset! *shown-datepicker nil)
|
||||
(swap! *between-dates assoc id journal-date)))}))]))
|
||||
|
||||
(rum/defcs between <
|
||||
(rum/local nil ::start)
|
||||
(rum/local nil ::end)
|
||||
[state {:keys [tree loc] :as opts}]
|
||||
[:div.between-date {:on-mouse-down (fn [e] (util/stop-propagation e))}
|
||||
[:div.flex.flex-row
|
||||
[:div.font-medium.mt-2 "Between: "]
|
||||
(datepicker :start "Start date" (merge opts {:auto-focus true}))
|
||||
(datepicker :end "End date" opts)]
|
||||
(ui/button "Submit"
|
||||
:on-click (fn []
|
||||
(let [{:keys [start end]} @*between-dates]
|
||||
(when (and start end)
|
||||
(let [clause [:between start end]]
|
||||
(append-tree! tree opts loc clause)
|
||||
(reset! *between-dates {}))))))])
|
||||
|
||||
(defn- query-filter-picker
|
||||
[state *find *tree loc clause opts]
|
||||
(let [*mode (::mode state)
|
||||
*property (::property state)
|
||||
repo (state/get-current-repo)]
|
||||
[:div
|
||||
(case @*mode
|
||||
"namespace"
|
||||
(let [items (sort (db-model/get-all-namespace-parents repo))]
|
||||
(select items
|
||||
(fn [value]
|
||||
(append-tree! *tree opts loc [:namespace value]))))
|
||||
|
||||
"tags"
|
||||
(let [items (->> (db-model/get-all-tagged-pages repo)
|
||||
(map second)
|
||||
sort)]
|
||||
(select items
|
||||
(fn [value]
|
||||
(append-tree! *tree opts loc [:page-tags value]))))
|
||||
|
||||
"property"
|
||||
(let [properties (search/get-all-properties)]
|
||||
(select properties
|
||||
(fn [value]
|
||||
(reset! *mode "property-value")
|
||||
(reset! *property (keyword value)))))
|
||||
|
||||
"property-value"
|
||||
(let [values (cons "Select all" (db-model/get-property-values @*property))]
|
||||
(select values
|
||||
(fn [value]
|
||||
(let [x (if (= value "Select all")
|
||||
[(if (= @*find :page) :page-property :property) @*property]
|
||||
[(if (= @*find :page) :page-property :property) @*property value])]
|
||||
(reset! *property nil)
|
||||
(append-tree! *tree opts loc x)))))
|
||||
|
||||
"sample"
|
||||
(select (range 1 101)
|
||||
(fn [value]
|
||||
(append-tree! *tree opts loc [:sample (util/safe-parse-int value)])))
|
||||
|
||||
"task"
|
||||
(select db-default/built-in-markers
|
||||
(fn [value]
|
||||
(when (seq value)
|
||||
(append-tree! *tree opts loc (vec (cons :task value)))))
|
||||
{:multiple-choices? true
|
||||
;; Need the existing choices later to improve the UX
|
||||
:selected-choices #{}
|
||||
:prompt-key :select/default-select-multiple
|
||||
:close-modal? false})
|
||||
|
||||
"priority"
|
||||
(select db-default/built-in-priorities
|
||||
(fn [value]
|
||||
(when (seq value)
|
||||
(append-tree! *tree opts loc (vec (cons :priority value)))))
|
||||
{:multiple-choices? true
|
||||
:selected-choices #{}
|
||||
:prompt-key :select/default-select-multiple
|
||||
:close-modal? false})
|
||||
|
||||
"page"
|
||||
(let [pages (sort (db-model/get-all-page-original-names repo))]
|
||||
(select pages
|
||||
(fn [value]
|
||||
(append-tree! *tree opts loc [:page value]))))
|
||||
|
||||
"page reference"
|
||||
(let [pages (sort (db-model/get-all-page-original-names repo))]
|
||||
(select pages
|
||||
(fn [value]
|
||||
(append-tree! *tree opts loc [:page-ref value]))
|
||||
{}))
|
||||
|
||||
"full text search"
|
||||
(search (fn [v] (append-tree! *tree opts loc v))
|
||||
(:toggle-fn opts))
|
||||
|
||||
"between"
|
||||
(between (merge opts
|
||||
{:tree *tree
|
||||
:loc loc
|
||||
:clause clause}))
|
||||
|
||||
nil)]))
|
||||
|
||||
(rum/defcs picker <
|
||||
{:will-mount (fn [state]
|
||||
(state/clear-selection!)
|
||||
state)}
|
||||
(rum/local nil ::mode) ; pick mode
|
||||
(rum/local nil ::property)
|
||||
[state *find *tree loc clause opts]
|
||||
(let [*mode (::mode state)
|
||||
filters (if (= :page @*find)
|
||||
query-builder/page-filters
|
||||
query-builder/block-filters)
|
||||
filters-and-ops (concat filters query-builder/operators)
|
||||
operator? #(contains? query-builder/operators-set (keyword %))]
|
||||
[:div.query-builder-picker
|
||||
(if @*mode
|
||||
(when-not (operator? @*mode)
|
||||
(query-filter-picker state *find *tree loc clause opts))
|
||||
[:div
|
||||
(when-not @*find
|
||||
[:div.flex.flex-row.items-center.p-2.justify-between
|
||||
[:div.ml-2 "Find: "]
|
||||
(page-block-selector *find)])
|
||||
(when-not @*find
|
||||
[:hr.m-0])
|
||||
(select
|
||||
(map name filters-and-ops)
|
||||
(fn [value]
|
||||
(cond
|
||||
(= value "all page tags")
|
||||
(append-tree! *tree opts loc [:all-page-tags])
|
||||
|
||||
(operator? value)
|
||||
(append-tree! *tree opts loc [(keyword value)])
|
||||
|
||||
:else
|
||||
(reset! *mode value)))
|
||||
{:input-default-placeholder "Add filter/operator"})])]))
|
||||
|
||||
(rum/defc add-filter
|
||||
[*find *tree loc clause]
|
||||
(ui/dropdown
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
[:a.flex.add-filter {:title "Add clause"
|
||||
:on-click toggle-fn}
|
||||
(ui/icon "plus" {:style {:font-size 20}})])
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
(picker *find *tree loc clause {:toggle-fn toggle-fn}))
|
||||
{:modal-class (util/hiccup->class
|
||||
"origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg")}))
|
||||
|
||||
(declare clauses-group)
|
||||
|
||||
(defn- dsl-human-output
|
||||
[clause]
|
||||
(let [f (first clause)]
|
||||
(cond
|
||||
(string? clause)
|
||||
(str "search: " clause)
|
||||
|
||||
(= (keyword f) :page-ref)
|
||||
(page-ref/->page-ref (second clause))
|
||||
|
||||
(= (keyword f) :page-tags)
|
||||
(if (string? (second clause))
|
||||
(str "#" (second clause))
|
||||
(str "#" (second (second clause))))
|
||||
|
||||
(contains? #{:property :page-property} (keyword f))
|
||||
(str (name (second clause)) ": "
|
||||
(cond
|
||||
(and (vector? (last clause)) (= :page-ref (first (last clause))))
|
||||
(second (last clause))
|
||||
|
||||
(= 2 (count clause))
|
||||
"ALL"
|
||||
|
||||
:else
|
||||
(last clause)))
|
||||
|
||||
(= (keyword f) :between)
|
||||
(str "between: " (second (second clause)) " - " (second (last clause)))
|
||||
|
||||
(contains? #{:task :priority} (keyword f))
|
||||
(str (name f) ": "
|
||||
(string/join " | " (rest clause)))
|
||||
|
||||
(contains? #{:page :task :namespace} (keyword f))
|
||||
(str (name f) ": " (if (vector? (second clause))
|
||||
(second (second clause))
|
||||
(second clause)))
|
||||
|
||||
(= 2 (count clause))
|
||||
(str (name f) ": " (second clause))
|
||||
|
||||
:else
|
||||
(str (query-builder/->dsl clause)))))
|
||||
|
||||
(rum/defc clause-inner
|
||||
[*tree loc clause & {:keys [operator?]}]
|
||||
(ui/dropdown
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
(if operator?
|
||||
[:a.flex.text-sm.query-clause {:on-click toggle-fn}
|
||||
clause]
|
||||
|
||||
[:div.flex.flex-row.items-center.gap-2.p-1.rounded.border
|
||||
[:a.flex.query-clause {:on-click toggle-fn}
|
||||
(dsl-human-output clause)]]))
|
||||
(fn [{:keys [toggle-fn]}]
|
||||
[:div.p-4.flex.flex-col.gap-2
|
||||
[:a {:title "Delete"
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(let [loc' (if operator? (vec (butlast loc)) loc)]
|
||||
(query-builder/remove-element q loc'))))
|
||||
(toggle-fn))}
|
||||
"Delete"]
|
||||
|
||||
(when operator?
|
||||
[:a {:title "Unwrap this operator"
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(let [loc' (vec (butlast loc))]
|
||||
(query-builder/unwrap-operator q loc'))))
|
||||
(toggle-fn))}
|
||||
"Unwrap"])
|
||||
|
||||
[:div.font-medium.text-sm "Wrap this filter with: "]
|
||||
[:div.flex.flex-row.gap-2
|
||||
(for [op query-builder/operators]
|
||||
(ui/button (string/upper-case (name op))
|
||||
:intent "logseq"
|
||||
:small? true
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(let [loc' (if operator? (vec (butlast loc)) loc)]
|
||||
(query-builder/wrap-operator q loc' op))))
|
||||
(toggle-fn))))]
|
||||
|
||||
(when operator?
|
||||
[:div
|
||||
[:div.font-medium.text-sm "Replace with: "]
|
||||
[:div.flex.flex-row.gap-2
|
||||
(for [op (remove #{(keyword (string/lower-case clause))} query-builder/operators)]
|
||||
(ui/button (string/upper-case (name op))
|
||||
:intent "logseq"
|
||||
:small? true
|
||||
:on-click (fn []
|
||||
(swap! *tree (fn [q]
|
||||
(query-builder/replace-element q loc op)))
|
||||
(toggle-fn))))]])])
|
||||
{:modal-class (util/hiccup->class
|
||||
"origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg.w-64")}))
|
||||
|
||||
(rum/defc clause
|
||||
[*tree *find loc clause]
|
||||
(when (seq clause)
|
||||
[:div.query-builder-clause
|
||||
(let [kind (keyword (first clause))]
|
||||
(if (query-builder/operators-set kind)
|
||||
[:div.operator-clause.flex.flex-row.items-center {:data-level (count loc)}
|
||||
[:div.text-4xl.mr-1.font-thin "("]
|
||||
(clauses-group *tree *find (conj loc 0) kind (rest clause))
|
||||
[:div.text-4xl.ml-1.font-thin ")"]]
|
||||
(clause-inner *tree loc clause)))]))
|
||||
|
||||
(rum/defc clauses-group
|
||||
[*tree *find loc kind clauses]
|
||||
(let [parens? (and (= loc [0])
|
||||
(> (count clauses) 1))]
|
||||
[:div.clauses-group
|
||||
(when parens? [:div.text-4xl.mr-1.font-thin "("])
|
||||
(when-not (and (= loc [0])
|
||||
(= kind :and)
|
||||
(<= (count clauses) 1))
|
||||
(clause-inner *tree loc
|
||||
(string/upper-case (name kind))
|
||||
:operator? true))
|
||||
|
||||
(map-indexed (fn [i item]
|
||||
(clause *tree *find (update loc (dec (count loc)) #(+ % i 1)) item))
|
||||
clauses)
|
||||
|
||||
(when parens? [:div.text-4xl.ml-1.font-thin ")"])
|
||||
|
||||
(when (not= loc [0])
|
||||
(add-filter *find *tree loc []))]))
|
||||
|
||||
(rum/defc clause-tree < rum/reactive
|
||||
[*tree *find]
|
||||
(let [tree (rum/react *tree)
|
||||
kind ((set query-builder/operators) (first tree))
|
||||
[kind' clauses] (if kind
|
||||
[kind (rest tree)]
|
||||
[:and [@tree]])]
|
||||
(clauses-group *tree *find [0] kind' clauses)))
|
||||
|
||||
(rum/defcs builder <
|
||||
(rum/local nil ::find)
|
||||
{:init (fn [state]
|
||||
(let [q-str (first (:rum/args state))
|
||||
query (gp-util/safe-read-string
|
||||
query-dsl/custom-readers
|
||||
(query-dsl/pre-transform-query q-str))
|
||||
query' (cond
|
||||
(contains? #{'and 'or 'not} (first query))
|
||||
query
|
||||
|
||||
query
|
||||
[:and query]
|
||||
|
||||
:else
|
||||
[:and])
|
||||
tree (query-builder/from-dsl query')
|
||||
*tree (atom tree)
|
||||
config (last (:rum/args state))]
|
||||
(add-watch *tree :updated (fn [_ _ _old _new]
|
||||
(when-let [block (:block config)]
|
||||
(let [q (if (= [:and] @*tree)
|
||||
""
|
||||
(let [result (query-builder/->dsl @*tree)]
|
||||
(if (string? result)
|
||||
(util/format "\"%s\"" result)
|
||||
(str result))))
|
||||
repo (state/get-current-repo)
|
||||
block (db/pull [:block/uuid (:block/uuid block)])]
|
||||
(when block
|
||||
(let [content (string/replace (:block/content block)
|
||||
(util/format "{{query %s" q-str)
|
||||
(util/format "{{query %s" q))]
|
||||
(editor-handler/save-block! repo (:block/uuid block) content)))))))
|
||||
(assoc state ::tree *tree)))
|
||||
:will-mount (fn [state]
|
||||
(let [q-str (first (:rum/args state))
|
||||
parsed-query (query-dsl/parse-query q-str)
|
||||
blocks-query? (:blocks? parsed-query)
|
||||
find-mode (cond
|
||||
blocks-query?
|
||||
:block
|
||||
(false? blocks-query?)
|
||||
:page
|
||||
:else
|
||||
nil)]
|
||||
(when find-mode (reset! (::find state) find-mode))
|
||||
state))}
|
||||
[state _query _config]
|
||||
(let [*find (::find state)
|
||||
*tree (::tree state)]
|
||||
[:div.cp__query-builder
|
||||
[:div.cp__query-builder-filter
|
||||
(when (and (seq @*tree)
|
||||
(not= @*tree [:and]))
|
||||
(clause-tree *tree *find))
|
||||
(add-filter *find *tree [0] [])]]))
|
||||
46
src/main/frontend/components/query/builder.css
Normal file
46
src/main/frontend/components/query/builder.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.cp__query-builder {
|
||||
@apply grid auto-rows-max gap-2;
|
||||
|
||||
&-filter {
|
||||
@apply flex flex-row items-center gap-1;
|
||||
}
|
||||
|
||||
.cp__select-main {
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.between-date {
|
||||
min-width: 36em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.cp__select .input-wrap {
|
||||
height: auto;
|
||||
min-width: 14em;
|
||||
}
|
||||
|
||||
.cp__select .input-wrap input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cp__select-input {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.clauses-group {
|
||||
@apply flex flex-row gap-1 flex-wrap items-center text-sm;
|
||||
}
|
||||
|
||||
a.query-clause, a.add-filter {
|
||||
color: var(--ls-primary-text-color);
|
||||
}
|
||||
|
||||
a.query-clause:hover, a.add-filter {
|
||||
color: var(--ls-secondary-text-color);
|
||||
}
|
||||
|
||||
.filter-item select {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,7 @@
|
||||
(defn- get-columns [current-block result {:keys [page?]}]
|
||||
(let [query-properties (some-> (get-in current-block [:block/properties :query-properties] "")
|
||||
(common-handler/safe-read-string "Parsing query properties failed"))
|
||||
query-properties (if page? (remove #{:block} query-properties) query-properties)
|
||||
columns (if (seq query-properties)
|
||||
query-properties
|
||||
(get-keys result page?))
|
||||
@@ -114,10 +115,12 @@
|
||||
;; Table rows are called items
|
||||
(rum/defcs result-table < rum/reactive
|
||||
(rum/local false ::select?)
|
||||
(rum/local false ::mouse-down?)
|
||||
[state config current-block result {:keys [page?]} map-inline page-cp ->elem inline-text]
|
||||
(when current-block
|
||||
(let [result (tree/filter-top-level-blocks result)
|
||||
select? (get state ::select?)
|
||||
*mouse-down? (::mouse-down? state)
|
||||
;; remove templates
|
||||
result (remove (fn [b] (some? (get-in b [:block/properties :template]))) result)
|
||||
result (if page? result (attach-clock-property result))
|
||||
@@ -173,14 +176,17 @@
|
||||
[:string (or (get-in item [:block/properties-text-values column])
|
||||
;; Fallback to property relationships for page blocks
|
||||
(get-in item [:block/properties column]))])]
|
||||
[:td.whitespace-nowrap {:on-mouse-down (fn [] (reset! select? false))
|
||||
[:td.whitespace-nowrap {:on-mouse-down (fn []
|
||||
(reset! *mouse-down? true)
|
||||
(reset! select? false))
|
||||
:on-mouse-move (fn [] (reset! select? true))
|
||||
:on-mouse-up (fn []
|
||||
(when-not @select?
|
||||
(when (and @*mouse-down? (not @select?))
|
||||
(state/sidebar-add-block!
|
||||
(state/get-current-repo)
|
||||
(:db/id item)
|
||||
:block-ref)))}
|
||||
:block-ref)
|
||||
(reset! *mouse-down? false)))}
|
||||
(when value
|
||||
(if (= :element (first value))
|
||||
(second value)
|
||||
|
||||
@@ -16,36 +16,45 @@
|
||||
[frontend.handler.repo :as repo-handler]
|
||||
[reitit.frontend.easy :as rfe]))
|
||||
|
||||
(rum/defc render-item
|
||||
[result chosen?]
|
||||
(if (map? result)
|
||||
(let [{:keys [id value]} result]
|
||||
[:div.inline-grid.grid-cols-4.gap-x-4.w-full
|
||||
{:class (when chosen? "chosen")}
|
||||
[:span.col-span-3 value]
|
||||
[:div.col-span-1.justify-end.tip.flex
|
||||
(when id
|
||||
[:code.opacity-20.bg-transparent id])]])
|
||||
[:div.inline-grid.grid-cols-4.gap-x-4.w-full
|
||||
{:class (when chosen? "chosen")}
|
||||
[:span.col-span-3 result]]))
|
||||
(rum/defc render-item < rum/reactive
|
||||
[result chosen? multiple-choices? *selected-choices]
|
||||
(let [value (if (map? result) (:value result) result)
|
||||
selected-choices (rum/react *selected-choices)]
|
||||
[:div.flex.flex-row.justify-between.w-full {:class (when chosen? "chosen")}
|
||||
[:span
|
||||
(when multiple-choices? (ui/checkbox {:checked (selected-choices value)
|
||||
:style {:margin-right 4}
|
||||
:on-click (fn [e]
|
||||
(.preventDefault e))}))
|
||||
value]
|
||||
(when (and (map? result) (:id result))
|
||||
[:div.tip.flex
|
||||
[:code.opacity-20.bg-transparent (:id result)]])]))
|
||||
|
||||
(rum/defcs select <
|
||||
(rum/defcs select < rum/reactive
|
||||
(shortcut/disable-all-shortcuts)
|
||||
(rum/local "" ::input)
|
||||
{:will-unmount (fn [state]
|
||||
{:init (fn [state]
|
||||
(assoc state ::selected-choices
|
||||
(atom (set (:selected-choices (first (:rum/args state)))))))
|
||||
:will-unmount (fn [state]
|
||||
(state/set-state! [:ui/open-select] nil)
|
||||
(let [{:keys [multiple-choices? on-chosen]} (first (:rum/args state))]
|
||||
(when (and multiple-choices? on-chosen)
|
||||
(on-chosen @(::selected-choices state))))
|
||||
state)}
|
||||
[state {:keys [items limit on-chosen empty-placeholder
|
||||
prompt-key input-default-placeholder close-modal?
|
||||
extract-fn host-opts on-input input-opts
|
||||
item-cp transform-fn tap-*input-val]
|
||||
item-cp transform-fn tap-*input-val
|
||||
multiple-choices? _selected-choices]
|
||||
:or {limit 100
|
||||
prompt-key :select/default-prompt
|
||||
empty-placeholder (fn [_t] [:div])
|
||||
close-modal? true
|
||||
extract-fn :value}}]
|
||||
(let [input (::input state)]
|
||||
(let [input (::input state)
|
||||
*selected-choices (::selected-choices state)]
|
||||
(when (fn? tap-*input-val)
|
||||
(tap-*input-val input))
|
||||
[:div.cp__select
|
||||
@@ -68,11 +77,19 @@
|
||||
(fn? transform-fn)
|
||||
(transform-fn @input))
|
||||
|
||||
{:item-render (or item-cp render-item)
|
||||
{:item-render (or item-cp (fn [result chosen?]
|
||||
(render-item result chosen? multiple-choices? *selected-choices)))
|
||||
:class "cp__select-results"
|
||||
:on-chosen (fn [x]
|
||||
(when close-modal? (state/close-modal!))
|
||||
(on-chosen x))
|
||||
(reset! input "")
|
||||
(if multiple-choices?
|
||||
(if (@*selected-choices x)
|
||||
(swap! *selected-choices disj x)
|
||||
(swap! *selected-choices conj x))
|
||||
(do
|
||||
(when close-modal? (state/close-modal!))
|
||||
(when on-chosen
|
||||
(on-chosen (if multiple-choices? @*selected-choices x))))))
|
||||
:empty-placeholder (empty-placeholder t)})]]))
|
||||
|
||||
(defn select-config
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
|
||||
(def close (hero-icon "M6 18L18 6M6 6L18 18"))
|
||||
(def folder (hero-icon "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"))
|
||||
(def settings-sm [:svg {:viewBox "0 0 20 20", :fill "currentColor", :height "20", :width "20"}
|
||||
[:path {:fill-rule "evenodd", :d "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z", :clip-rule "evenodd"}]])
|
||||
|
||||
(def external-link
|
||||
[:svg {:fill "none", :view-box "0 0 24 24", :height "21", :width "21"
|
||||
|
||||
@@ -48,6 +48,7 @@ html {
|
||||
border: none;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.form-checkbox:hover {
|
||||
@@ -167,4 +168,4 @@ main.ls-fold-button-on-right {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,12 @@
|
||||
[?page :block/name ?page-name]
|
||||
[?page :block/namespace ?e]
|
||||
[?e :block/name ?parent]]
|
||||
(conn/get-db repo)))
|
||||
(conn/get-db repo)))
|
||||
|
||||
(defn get-all-namespace-parents
|
||||
[repo]
|
||||
(->> (get-all-namespace-relation repo)
|
||||
(map second)))
|
||||
|
||||
(defn get-pages
|
||||
[repo]
|
||||
@@ -122,7 +127,13 @@
|
||||
'[:find [(pull ?page [*]) ...]
|
||||
:where
|
||||
[?page :block/name]]
|
||||
(conn/get-db repo)))
|
||||
(conn/get-db repo)))
|
||||
|
||||
(defn get-all-page-original-names
|
||||
[repo]
|
||||
(let [db (conn/get-db repo)]
|
||||
(->> (d/datoms db :avet :block/original-name)
|
||||
(map :v))))
|
||||
|
||||
(defn get-pages-with-file
|
||||
"Return full file entity for calling file renaming"
|
||||
|
||||
@@ -14,15 +14,19 @@
|
||||
[frontend.template :as template]
|
||||
[logseq.graph-parser.text :as text]
|
||||
[logseq.graph-parser.util.page-ref :as page-ref]
|
||||
[logseq.graph-parser.util :as gp-util]
|
||||
[frontend.util.text :as text-util]
|
||||
[frontend.util :as util]))
|
||||
|
||||
|
||||
;; Query fields:
|
||||
|
||||
;; Operators:
|
||||
;; and
|
||||
;; or
|
||||
;; not
|
||||
|
||||
;; Filters:
|
||||
;; between
|
||||
;; Example: (between -7d +7d)
|
||||
;; (between created-at -1d today)
|
||||
@@ -32,16 +36,17 @@
|
||||
;; task (block)
|
||||
;; priority (block)
|
||||
;; page
|
||||
;; sample
|
||||
;; full-text-search ""
|
||||
|
||||
;; namespace
|
||||
;; page-property (page)
|
||||
;; page-tags (page)
|
||||
;; all-page-tags
|
||||
;; project (block, TBD)
|
||||
|
||||
;; Sort by (field, asc/desc):
|
||||
;; (sort-by created-at asc)
|
||||
|
||||
;; (between -7d +7d)
|
||||
|
||||
;; Time helpers
|
||||
;; ============
|
||||
(defn- ->journal-day-int [input]
|
||||
@@ -443,23 +448,32 @@ Some bindings in this fn:
|
||||
;; parse fns
|
||||
;; =========
|
||||
|
||||
(defn- pre-transform
|
||||
(defonce tag-placeholder "~~~tag-placeholder~~~")
|
||||
(defn pre-transform
|
||||
[s]
|
||||
(let [quoted-page-ref (str "\"" page-ref/left-brackets "$1" page-ref/right-brackets "\"")]
|
||||
(some-> s
|
||||
(string/replace page-ref/page-ref-re quoted-page-ref)
|
||||
(string/replace text-util/between-re
|
||||
(fn [[_ x]]
|
||||
(->> (string/split x #" ")
|
||||
(remove string/blank?)
|
||||
(map (fn [x]
|
||||
(if (or (contains? #{"+" "-"} (first x))
|
||||
(and (util/safe-re-find #"\d" (first x))
|
||||
(some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
|
||||
(keyword (name x))
|
||||
x)))
|
||||
(string/join " ")
|
||||
(util/format "(between %s)")))))))
|
||||
(if (gp-util/wrapped-by-quotes? s)
|
||||
s
|
||||
(let [quoted-page-ref (fn [matches]
|
||||
(let [match' (string/replace (second matches) "#" tag-placeholder)]
|
||||
(str "\"" page-ref/left-brackets match' page-ref/right-brackets "\"")))]
|
||||
(some-> s
|
||||
(string/replace page-ref/page-ref-re quoted-page-ref)
|
||||
(string/replace text-util/between-re
|
||||
(fn [[_ x]]
|
||||
(->> (string/split x #" ")
|
||||
(remove string/blank?)
|
||||
(map (fn [x]
|
||||
(if (or (contains? #{"+" "-"} (first x))
|
||||
(and (util/safe-re-find #"\d" (first x))
|
||||
(some #(string/ends-with? x %) ["y" "m" "d" "h" "min"])))
|
||||
(keyword (name x))
|
||||
x)))
|
||||
(string/join " ")
|
||||
(util/format "(between %s)"))))
|
||||
(string/replace #"\"[^\"]+\"" (fn [s] (string/replace s "#" tag-placeholder)))
|
||||
(string/replace " #" " #tag ")
|
||||
(string/replace #"^#" "#tag ")
|
||||
(string/replace tag-placeholder "#")))))
|
||||
|
||||
(defn- add-bindings!
|
||||
[form q]
|
||||
@@ -499,17 +513,33 @@ Some bindings in this fn:
|
||||
:else
|
||||
q)))
|
||||
|
||||
(defn simplify-query
|
||||
[query]
|
||||
(if (string? query)
|
||||
query
|
||||
(walk/postwalk
|
||||
(fn [f]
|
||||
(if (and
|
||||
(coll? f)
|
||||
(contains? #{'and 'or} (first f))
|
||||
(= 2 (count f)))
|
||||
(second f)
|
||||
f))
|
||||
query)))
|
||||
|
||||
(def custom-readers {:readers {'tag (fn [x] (page-ref/->page-ref x))}})
|
||||
(defn parse
|
||||
[s]
|
||||
(when (and (string? s)
|
||||
(not (string/blank? s)))
|
||||
(let [s (if (= \# (first s)) (page-ref/->page-ref (subs s 1)) s)
|
||||
form (some-> s
|
||||
(pre-transform)
|
||||
(reader/read-string))
|
||||
form (some->> s
|
||||
(pre-transform)
|
||||
(reader/read-string custom-readers))
|
||||
sort-by (atom nil)
|
||||
blocks? (atom nil)
|
||||
sample (atom nil)
|
||||
form (simplify-query form)
|
||||
{result :query rules :rules}
|
||||
(when form (build-query form {:sort-by sort-by
|
||||
:blocks? blocks?
|
||||
@@ -545,12 +575,21 @@ Some bindings in this fn:
|
||||
(apply conj q where)
|
||||
(conj q where))))
|
||||
|
||||
(defn parse-query
|
||||
[q]
|
||||
(let [q' (template/resolve-dynamic-template! q)]
|
||||
(parse q')))
|
||||
|
||||
(defn pre-transform-query
|
||||
[q]
|
||||
(let [q' (template/resolve-dynamic-template! q)]
|
||||
(pre-transform q')))
|
||||
|
||||
(defn query
|
||||
"Runs a dsl query with query as a string. Primary use is from '{{query }}'"
|
||||
[repo query-string]
|
||||
(when (and (string? query-string) (not= "\"\"" query-string))
|
||||
(let [query-string' (template/resolve-dynamic-template! query-string)
|
||||
{:keys [query rules sort-by blocks? sample]} (parse query-string')]
|
||||
(let [{:keys [query rules sort-by blocks? sample]} (parse-query query-string)]
|
||||
(when-let [query' (some-> query (query-wrapper {:blocks? blocks?}))]
|
||||
(let [sort-by (or sort-by identity)
|
||||
random-samples (if @sample
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
resolved-inputs (mapv #(resolve-input db % resolve-with) inputs)
|
||||
inputs (cond-> resolved-inputs
|
||||
rules
|
||||
(conj rules))
|
||||
(conj rules))
|
||||
k [:custom (or (:query-string query') query') inputs]]
|
||||
(pprint "inputs (post-resolution):" resolved-inputs)
|
||||
(pprint "query-opts:" query-opts)
|
||||
|
||||
@@ -81,11 +81,6 @@
|
||||
(let [new-result' (f @result-atom)]
|
||||
(reset! result-atom new-result'))))
|
||||
|
||||
(defn get-query-time
|
||||
[q]
|
||||
(let [k [(state/get-current-repo) :custom q]]
|
||||
(get-in @query-state [k :query-time])))
|
||||
|
||||
(defn kv
|
||||
[key value]
|
||||
{:db/id -1
|
||||
@@ -113,12 +108,12 @@
|
||||
[k query time inputs result-atom transform-fn query-fn inputs-fn]
|
||||
(let [time' (int (util/safe-parse-float time))] ;; for robustness. `time` should already be float
|
||||
(swap! query-state assoc k {:query query
|
||||
:query-time time'
|
||||
:inputs inputs
|
||||
:result result-atom
|
||||
:transform-fn transform-fn
|
||||
:query-fn query-fn
|
||||
:inputs-fn inputs-fn}))
|
||||
:query-time time'
|
||||
:inputs inputs
|
||||
:result result-atom
|
||||
:transform-fn transform-fn
|
||||
:query-fn query-fn
|
||||
:inputs-fn inputs-fn}))
|
||||
result-atom)
|
||||
|
||||
(defn remove-q!
|
||||
@@ -184,7 +179,7 @@
|
||||
transform-fn))
|
||||
result-atom (or result-atom (atom nil))]
|
||||
;; Don't notify watches now
|
||||
(set! (.-state result-atom) result)
|
||||
(set! (.-state result-atom) (util/safe-with-meta result {:query-time time}))
|
||||
(if disable-reactive?
|
||||
result-atom
|
||||
(add-q! k query time inputs result-atom transform-fn query-fn inputs-fn))))))))
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
|
||||
:command-palette/prompt "Type a command"
|
||||
:select/default-prompt "Select one"
|
||||
:select/default-select-multiple "Select one or multiple"
|
||||
:select.graph/prompt "Select a graph"
|
||||
:select.graph/empty-placeholder-description "No matched graphs. Do you want to add another one?"
|
||||
:select.graph/add-graph "Yes, add another graph"
|
||||
|
||||
@@ -122,10 +122,10 @@ Remember: You can paste a raw YouTube url as embedded video on mobile."
|
||||
(let [reg #"^(?:(\d+):)?([0-5]\d):([0-5]\d)$"
|
||||
reg-number #"^\d+$"
|
||||
timestamp (str timestamp)
|
||||
total-seconds (-> (re-matches reg-number timestamp)
|
||||
util/safe-parse-int)
|
||||
total-seconds (some-> (re-matches reg-number timestamp)
|
||||
util/safe-parse-int)
|
||||
[_ hours minutes seconds] (re-matches reg timestamp)
|
||||
[hours minutes seconds] (map util/safe-parse-int [hours minutes seconds])]
|
||||
[hours minutes seconds] (map util/safe-parse-int (remove nil? [hours minutes seconds]))]
|
||||
(cond
|
||||
total-seconds
|
||||
total-seconds
|
||||
@@ -133,6 +133,12 @@ Remember: You can paste a raw YouTube url as embedded video on mobile."
|
||||
(and minutes seconds)
|
||||
(+ (* 3600 hours) (* 60 minutes) seconds)
|
||||
|
||||
minutes
|
||||
(+ (* 3600 hours) (* 60 minutes))
|
||||
|
||||
hours
|
||||
(* 3600 hours)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
|
||||
@@ -80,13 +80,18 @@
|
||||
[{:keys [id] :as command}]
|
||||
(if (:command/shortcut command)
|
||||
(log/error :shortcut/missing (str "Shortcut is missing for " (:id command)))
|
||||
(do
|
||||
(try
|
||||
(spec/validate :command/command command)
|
||||
(let [cmds (get-commands)]
|
||||
(if (some (fn [existing-cmd] (= (:id existing-cmd) id)) cmds)
|
||||
(log/error :command/register {:msg "Failed to register command. Command with same id already exist"
|
||||
:id id})
|
||||
(state/set-state! :command-palette/commands (conj cmds command)))))))
|
||||
(state/set-state! :command-palette/commands (conj cmds command))))
|
||||
;; Catch unexpected errors so that subsequent register calls pass
|
||||
(catch :default e
|
||||
(log/error :command/register {:msg "Unexpectedly failed to register command"
|
||||
:id id
|
||||
:error (str e)})))))
|
||||
|
||||
(defn unregister
|
||||
[id]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[frontend.db :as db]
|
||||
[frontend.db.model :as db-model]
|
||||
[frontend.db.utils :as db-utils]
|
||||
[frontend.db.query-dsl :as query-dsl]
|
||||
[frontend.diff :as diff]
|
||||
[frontend.format.block :as block]
|
||||
[frontend.format.mldoc :as mldoc]
|
||||
@@ -3215,6 +3216,20 @@
|
||||
(mldoc/block-with-title? first-elem-type))
|
||||
true)))
|
||||
|
||||
(defn- valid-dsl-query-block?
|
||||
"Whether block has a valid dsl query."
|
||||
[block]
|
||||
(->> (:block/macros (db/entity (:db/id block)))
|
||||
(some (fn [macro]
|
||||
(when-let [query-body (and
|
||||
(= "query" (get-in macro [:block/properties :logseq.macro-name]))
|
||||
(first (:logseq.macro-arguments (:block/properties macro))))]
|
||||
(seq (:query
|
||||
(try
|
||||
(query-dsl/parse-query query-body)
|
||||
(catch :default _e
|
||||
nil)))))))))
|
||||
|
||||
(defn collapsable?
|
||||
([block-id]
|
||||
(collapsable? block-id {}))
|
||||
@@ -3223,6 +3238,7 @@
|
||||
(when block-id
|
||||
(if-let [block (db-model/query-block-by-uuid block-id)]
|
||||
(or (db-model/has-children? block-id)
|
||||
(valid-dsl-query-block? block)
|
||||
(and
|
||||
(:outliner/block-title-collapse-enabled? (state/get-config))
|
||||
(block-with-title? (:block/format block)
|
||||
|
||||
197
src/main/frontend/handler/query/builder.cljs
Normal file
197
src/main/frontend/handler/query/builder.cljs
Normal file
@@ -0,0 +1,197 @@
|
||||
(ns frontend.handler.query.builder
|
||||
"DSL query builder handler"
|
||||
(:require [clojure.walk :as walk]
|
||||
[logseq.graph-parser.util.page-ref :as page-ref]
|
||||
[lambdaisland.glogi :as log]
|
||||
[frontend.db.query-dsl :as query-dsl]))
|
||||
|
||||
;; TODO: make it extensible for Datalog/SPARQL etc.
|
||||
|
||||
(def operators [:and :or :not])
|
||||
(def operators-set (set operators))
|
||||
(def page-filters ["all page tags"
|
||||
"namespace"
|
||||
"tags"
|
||||
"property"
|
||||
"sample"])
|
||||
(def block-filters ["page reference"
|
||||
"property"
|
||||
"task"
|
||||
"priority"
|
||||
"page"
|
||||
"full text search"
|
||||
"between"
|
||||
"sample"])
|
||||
|
||||
(defn- vec-dissoc-item
|
||||
[vec idx]
|
||||
(into (subvec vec 0 idx) (subvec vec (inc idx))))
|
||||
|
||||
(defn- vec-assoc-item
|
||||
[vec idx item]
|
||||
(into (conj (subvec vec 0 idx) item)
|
||||
(subvec vec idx)))
|
||||
|
||||
(defn- vec-replace-item
|
||||
[v idx item]
|
||||
(into (if (and (coll? item)
|
||||
(not (operators-set (first item))))
|
||||
(vec (concat (subvec v 0 idx) item))
|
||||
(conj (subvec v 0 idx) item))
|
||||
(subvec v (inc idx))))
|
||||
|
||||
(defn add-element
|
||||
[q loc x]
|
||||
{:pre [(vector? loc) (some? x)]}
|
||||
(cond
|
||||
(and (seq loc) (= 1 (count loc)))
|
||||
(vec-assoc-item q (first loc) x)
|
||||
|
||||
(seq loc)
|
||||
(update-in q (vec (butlast loc))
|
||||
(fn [v]
|
||||
(vec-assoc-item v (last loc) x)))
|
||||
|
||||
(seq q)
|
||||
(conj q x)
|
||||
|
||||
:else
|
||||
[x]))
|
||||
|
||||
(defn append-element
|
||||
[q loc x]
|
||||
{:pre [(vector? loc) (some? x)]}
|
||||
(let [idx (count (get-in q (vec (butlast loc))))
|
||||
loc' (vec-replace-item loc (dec (count loc)) idx)]
|
||||
(add-element q loc' x)))
|
||||
|
||||
(defn remove-element
|
||||
[q loc]
|
||||
(if (seq loc)
|
||||
(let [idx (last loc)
|
||||
ks (vec (butlast loc))
|
||||
f #(vec-dissoc-item % idx)]
|
||||
(if (seq ks)
|
||||
(let [result (update-in q ks f)]
|
||||
(if (seq (get-in result ks))
|
||||
result
|
||||
;; remove the wrapped empty vector
|
||||
(remove-element result ks)))
|
||||
(f q)))
|
||||
;; default to AND operator
|
||||
[:and]))
|
||||
|
||||
(defn replace-element
|
||||
[q loc x]
|
||||
{:pre [(vector? loc) (seq loc) (some? x)]}
|
||||
(if (= 1 (count loc))
|
||||
(vec-replace-item q (first loc) x)
|
||||
(update-in q (vec (butlast loc))
|
||||
(fn [v]
|
||||
(vec-replace-item v (last loc) x)))))
|
||||
|
||||
(defn- fallback-to-default [result default-value failed-data]
|
||||
(if (empty? result)
|
||||
(do
|
||||
(log/error :query-builder/wrap-unwrap-operator-failed failed-data)
|
||||
default-value)
|
||||
result))
|
||||
|
||||
(defn wrap-operator
|
||||
[q loc operator]
|
||||
{:pre [(seq q) (operators-set operator)]}
|
||||
(let [result (if (or (= loc [0]) (empty? loc))
|
||||
[operator q]
|
||||
(when-let [x (get-in q loc)]
|
||||
(let [x' [operator x]]
|
||||
(replace-element q loc x'))))]
|
||||
(fallback-to-default result q {:op "wrap-operator"
|
||||
:q q
|
||||
:loc loc
|
||||
:operator operator})))
|
||||
|
||||
(defn unwrap-operator
|
||||
[q loc]
|
||||
{:pre [(seq q) (seq loc)]}
|
||||
(let [result (if (and (= loc [0]) (operators-set (first q)))
|
||||
(second q)
|
||||
(when-let [x (get-in q loc)]
|
||||
(when (and (operators-set (first x))
|
||||
(seq (rest x)))
|
||||
(let [x' (rest x)]
|
||||
(replace-element q loc x')))))]
|
||||
(fallback-to-default result q {:op "unwrap-operator"
|
||||
:q q
|
||||
:loc loc})))
|
||||
|
||||
(defn ->page-ref
|
||||
[x]
|
||||
(if (string? x)
|
||||
(symbol (page-ref/->page-ref x))
|
||||
(->page-ref (second x))))
|
||||
|
||||
(defn- ->dsl*
|
||||
[f]
|
||||
(cond
|
||||
(and (vector? f) (= :priority (keyword (first f))))
|
||||
(vec (cons (symbol :priority) (map symbol (rest f))))
|
||||
|
||||
(and (vector? f) (= :task (keyword (first f))))
|
||||
(vec (cons (symbol :task) (map symbol (rest f))))
|
||||
|
||||
(and (vector? f) (= :page-ref (keyword (first f))))
|
||||
(->page-ref (second f))
|
||||
|
||||
(and (vector? f) (= :page-tags (keyword (first f))))
|
||||
[(symbol :page-tags) (->page-ref (second f))]
|
||||
|
||||
(and (vector? f) (= :between (keyword (first f))))
|
||||
(into [(symbol :between)] (map ->page-ref (rest f)))
|
||||
|
||||
;; property key value
|
||||
(and (vector? f) (= 3 (count f)) (contains? #{:page-property :property} (keyword (first f))))
|
||||
(let [l (if (page-ref/page-ref? (str (last f)))
|
||||
(symbol (last f))
|
||||
(last f))]
|
||||
(into [(symbol (first f))] [(second f) l]))
|
||||
|
||||
(and (vector? f) (contains? #{:page :namespace :tags} (keyword (first f))))
|
||||
(into [(symbol (first f))] (map ->page-ref (rest f)))
|
||||
|
||||
:else f))
|
||||
|
||||
(defn ->dsl
|
||||
[col]
|
||||
(->
|
||||
(walk/prewalk
|
||||
(fn [f]
|
||||
(let [f' (->dsl* f)]
|
||||
(cond
|
||||
(and (vector? f') (keyword (first f')))
|
||||
(cons (symbol (first f')) (rest f'))
|
||||
|
||||
:else f')))
|
||||
col)
|
||||
(query-dsl/simplify-query)))
|
||||
|
||||
(defn from-dsl
|
||||
[dsl-form]
|
||||
(walk/prewalk
|
||||
(fn [f]
|
||||
(cond
|
||||
(and (vector? f) (vector? (first f)))
|
||||
[:page-ref (page-ref/get-page-name (str f))]
|
||||
|
||||
(and (string? f) (page-ref/get-page-name f))
|
||||
[:page-ref (page-ref/get-page-name f)]
|
||||
|
||||
(and (list? f)
|
||||
(symbol? (first f))
|
||||
(operators-set (keyword (first f)))) ; operator
|
||||
(into [(keyword (first f))] (rest f))
|
||||
|
||||
(list? f)
|
||||
(vec f)
|
||||
|
||||
:else f))
|
||||
dsl-form))
|
||||
@@ -96,9 +96,7 @@
|
||||
(if clear-all-query-state?
|
||||
(db/clear-query-state!)
|
||||
(db/clear-query-state-without-refs-and-embeds!))
|
||||
(rum/request-render component)
|
||||
(doseq [component (state/get-custom-query-components)]
|
||||
(rum/request-render component)))))
|
||||
(rum/request-render component))))
|
||||
|
||||
(defn highlight-element!
|
||||
[fragment]
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
(events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))
|
||||
|
||||
(defn disable-all-shortcuts []
|
||||
{:did-mount
|
||||
{:will-mount
|
||||
(fn [state]
|
||||
(unlisten-all)
|
||||
state)
|
||||
|
||||
@@ -181,16 +181,20 @@
|
||||
(let [result (fuzzy-search (keys templates) q :limit limit)]
|
||||
(vec (select-keys templates result))))))))
|
||||
|
||||
(defn get-all-properties
|
||||
[]
|
||||
(->> (db-model/get-all-properties)
|
||||
(remove (property/hidden-properties))
|
||||
;; Complete full keyword except the ':'
|
||||
(map #(subs (str %) 1))))
|
||||
|
||||
(defn property-search
|
||||
([q]
|
||||
(property-search q 100))
|
||||
([q limit]
|
||||
(when q
|
||||
(let [q (clean-str q)
|
||||
properties (->> (db-model/get-all-properties)
|
||||
(remove (property/hidden-properties))
|
||||
;; Complete full keyword except the ':'
|
||||
(map #(subs (str %) 1)))]
|
||||
properties (get-all-properties)]
|
||||
(when (seq properties)
|
||||
(if (string/blank? q)
|
||||
properties
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
:ui/sidebar-collapsed-blocks {}
|
||||
:ui/root-component nil
|
||||
:ui/file-component nil
|
||||
:ui/custom-query-components {}
|
||||
:ui/show-recent? false
|
||||
:ui/developer-mode? (or (= (storage/get "developer-mode") "true")
|
||||
false)
|
||||
@@ -1254,22 +1253,6 @@ Similar to re-frame subscriptions"
|
||||
(when value
|
||||
(set-state! :journals-length value)))
|
||||
|
||||
(defn add-custom-query-component!
|
||||
[query-string component]
|
||||
(update-state! :ui/custom-query-components
|
||||
(fn [m]
|
||||
(assoc m query-string component))))
|
||||
|
||||
(defn remove-custom-query-component!
|
||||
[query-string]
|
||||
(update-state! :ui/custom-query-components
|
||||
(fn [m]
|
||||
(dissoc m query-string))))
|
||||
|
||||
(defn get-custom-query-components
|
||||
[]
|
||||
(vals (get @state :ui/custom-query-components)))
|
||||
|
||||
(defn save-scroll-position!
|
||||
([value]
|
||||
(save-scroll-position! value js/window.location.hash))
|
||||
|
||||
@@ -109,11 +109,12 @@
|
||||
:will-unmount (fn [state]
|
||||
(state/update-state! :modal/dropdowns #(dissoc % (::k state)))
|
||||
state)}
|
||||
[dropdown-state _close-fn content class]
|
||||
[dropdown-state _close-fn content class style-opts]
|
||||
(let [class (or class
|
||||
(util/hiccup->class "origin-top-right.absolute.right-0.mt-2"))]
|
||||
[:div.dropdown-wrapper
|
||||
{:class (str class " "
|
||||
{:style style-opts
|
||||
:class (str class " "
|
||||
(case dropdown-state
|
||||
"entering" "transition ease-out duration-100 transform opacity-0 scale-95"
|
||||
"entered" "transition ease-out duration-100 transform opacity-100 scale-100"
|
||||
@@ -129,13 +130,13 @@
|
||||
(let [{:keys [open?]} state
|
||||
modal-content (modal-content-fn state)
|
||||
close-fn (:close-fn state)]
|
||||
[:div.relative.ui__dropdown-trigger {:style {:z-index z-index} :class trigger-class}
|
||||
[:div.relative.ui__dropdown-trigger {:class trigger-class}
|
||||
(content-fn state)
|
||||
(css-transition
|
||||
{:in @open? :timeout 0}
|
||||
(fn [dropdown-state]
|
||||
(when @open?
|
||||
(dropdown-content-wrapper dropdown-state close-fn modal-content modal-class))))]))
|
||||
(dropdown-content-wrapper dropdown-state close-fn modal-content modal-class {:z-index z-index}))))]))
|
||||
|
||||
;; `sequence` can be a list of symbols, a list of strings, or a string
|
||||
(defn render-keyboard-shortcut [sequence]
|
||||
@@ -817,11 +818,11 @@
|
||||
([options on-change]
|
||||
(select options on-change nil))
|
||||
([options on-change class]
|
||||
[:select.pl-6.mt-1.block.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-1.sm:ml-4.w-12.sm:w-20
|
||||
[:select.pl-6.block.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5
|
||||
{:class (or class "form-select")
|
||||
:on-change (fn [e]
|
||||
(let [value (util/evalue e)]
|
||||
(on-change value)))}
|
||||
(on-change e value)))}
|
||||
(for [{:keys [label value selected disabled]
|
||||
:or {selected false disabled false}} options]
|
||||
[:option (cond->
|
||||
|
||||
@@ -404,3 +404,7 @@ html.is-mobile {
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
|
||||
.ui__radio-list {
|
||||
@apply grid grid-flow-col gap-2;
|
||||
}
|
||||
|
||||
@@ -1521,3 +1521,10 @@ Arg *stop: atom, reset to true to stop the loop"
|
||||
"Vector version of remove. non-lazy"
|
||||
[pred coll]
|
||||
`(vec (remove ~pred ~coll)))
|
||||
|
||||
#?(:cljs
|
||||
(defn safe-with-meta
|
||||
[o meta]
|
||||
(if (satisfies? IMeta o)
|
||||
(with-meta o meta)
|
||||
o)))
|
||||
|
||||
@@ -45,6 +45,30 @@
|
||||
;; Tests
|
||||
;; =====
|
||||
|
||||
(deftest pre-transform-test
|
||||
(testing "page references should be quoted and tags should be handled"
|
||||
(are [x y] (= (query-dsl/pre-transform x) y)
|
||||
"#foo"
|
||||
"#tag foo"
|
||||
|
||||
"(and #foo)"
|
||||
"(and #tag foo)"
|
||||
|
||||
"[[test #foo]]"
|
||||
"\"[[test #foo]]\""
|
||||
|
||||
"(and [[test #foo]] (or #foo))"
|
||||
"(and \"[[test #foo]]\" (or #tag foo))"
|
||||
|
||||
"\"for #clojure\""
|
||||
"\"for #clojure\""
|
||||
|
||||
"(and \"for #clojure\")"
|
||||
"(and \"for #clojure\")"
|
||||
|
||||
"(and \"for #clojure\" #foo)"
|
||||
"(and \"for #clojure\" #tag foo)")))
|
||||
|
||||
(defn- block-property-queries-test
|
||||
[]
|
||||
(load-test-files [{:file/path "journals/2022_02_28.md"
|
||||
@@ -529,6 +553,26 @@ created-at:: 1608968448116
|
||||
(->> (dsl-query "(and (page-property rating) (sort-by rating))")
|
||||
(map #(get-in % [:block/properties :rating])))))))
|
||||
|
||||
(deftest simplify-query
|
||||
(are [x y] (= (query-dsl/simplify-query x) y)
|
||||
'(and [[foo]])
|
||||
'[[foo]]
|
||||
|
||||
'(and (and [[foo]]))
|
||||
'[[foo]]
|
||||
|
||||
'(and (or [[foo]]))
|
||||
'[[foo]]
|
||||
|
||||
'(and (not [[foo]]))
|
||||
'(not [[foo]])
|
||||
|
||||
'(and (or (and [[foo]])))
|
||||
'[[foo]]
|
||||
|
||||
'(not (or [[foo]]))
|
||||
'(not [[foo]])))
|
||||
|
||||
(comment
|
||||
(require '[clojure.pprint :as pprint])
|
||||
(test-helper/start-test-db!)
|
||||
|
||||
38
src/test/frontend/handler/query/builder_test.cljs
Normal file
38
src/test/frontend/handler/query/builder_test.cljs
Normal file
@@ -0,0 +1,38 @@
|
||||
(ns frontend.handler.query.builder-test
|
||||
(:require [frontend.handler.query.builder :as query-builder]
|
||||
[clojure.test :refer [deftest is]]))
|
||||
|
||||
(deftest builder
|
||||
(let [q []]
|
||||
(is (= (query-builder/wrap-operator [:page-ref "foo"] [0] :and)
|
||||
[:and [:page-ref "foo"]]))
|
||||
(is (= (query-builder/unwrap-operator [:and [:page-ref "foo"]] [0])
|
||||
[:page-ref "foo"]))
|
||||
(is (= (-> (query-builder/add-element q [0] :and)
|
||||
(query-builder/add-element [1] [:page-ref "foo"])
|
||||
(query-builder/add-element [2] [:page-ref "bar"])
|
||||
(query-builder/wrap-operator [1] :or)
|
||||
(query-builder/unwrap-operator [1]))
|
||||
[:and [:page-ref "foo"] [:page-ref "bar"]]))
|
||||
(is (= (-> (query-builder/add-element q [0] :or)
|
||||
(query-builder/add-element [1] [:page-ref "foo"])
|
||||
(query-builder/add-element [2] [:page-ref "bar"])
|
||||
(query-builder/wrap-operator [2] :and)
|
||||
(query-builder/unwrap-operator [2]))
|
||||
[:or [:page-ref "foo"] [:page-ref "bar"]]))))
|
||||
|
||||
(deftest to-dsl
|
||||
(is (= (str (query-builder/->dsl [:and [:page-ref "foo"] [:page-ref "bar"]]))
|
||||
(str '(and [[foo]] [[bar]]))))
|
||||
(is (= (str (query-builder/->dsl [:and [:page-ref "foo"] [:or [:page-ref "bar"] [:property :key :value]]]))
|
||||
(str '(and [[foo]] (or [[bar]] (property :key :value))))))
|
||||
(is (= (str (query-builder/->dsl [:and [:priority "A"] [:task "NOW"]]))
|
||||
(str '(and (priority A) (task NOW))))))
|
||||
|
||||
(deftest from-dsl
|
||||
(is (= (query-builder/from-dsl '(and [[foo]] [[bar]]))
|
||||
[:and [:page-ref "foo"] [:page-ref "bar"]]))
|
||||
(is (= (query-builder/from-dsl '(and [[foo]] (or [[bar]] (:property :key :value))))
|
||||
[:and [:page-ref "foo"] [:or [:page-ref "bar"] [:property :key :value]]]))
|
||||
(is (= (query-builder/from-dsl '(and (priority A) (task NOW)))
|
||||
[:and ['priority 'A] ['task 'NOW]])))
|
||||
Reference in New Issue
Block a user