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:
Tienson Qin
2023-03-21 14:27:00 +08:00
committed by GitHub
parent 7a905583cf
commit db6fc6b7ee
37 changed files with 1263 additions and 295 deletions

View File

@@ -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

View File

@@ -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]

View File

@@ -118,6 +118,7 @@
:block/properties
:block/properties-order
:block/properties-text-values
:block/macros
:block/invalid-properties
:block/created-at
:block/updated-at

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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]})

View File

@@ -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))

View File

@@ -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]

View File

@@ -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;

View File

@@ -62,7 +62,7 @@
{:label "w"}
{:label "m"}
{:label "y"}])
(fn [value]
(fn [_e value]
(swap! *timestamp assoc-in [:repeater :duration] value))
nil)

View File

@@ -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

View File

@@ -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)}

View File

@@ -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

View 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] [])]]))

View 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;
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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;
}
}
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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)

View File

@@ -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))))))))

View File

@@ -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"

View File

@@ -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)))

View File

@@ -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]

View File

@@ -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)

View 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))

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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))

View File

@@ -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->

View File

@@ -404,3 +404,7 @@ html.is-mobile {
content: " ";
}
}
.ui__radio-list {
@apply grid grid-flow-col gap-2;
}

View File

@@ -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)))

View File

@@ -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!)

View 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]])))