From db6fc6b7ee93a48133917670f10e2c8144f3d05a Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Tue, 21 Mar 2023 14:27:00 +0800 Subject: [PATCH] feat: simple query builder (#8774) Simple query builder --------- Co-authored-by: Gabriel Horner Co-authored-by: charlie --- .clj-kondo/config.edn | 1 + deps/db/src/logseq/db/default.cljs | 14 +- deps/db/src/logseq/db/schema.cljs | 1 + .../src/logseq/graph_parser/util.cljs | 14 +- resources/css/common.css | 7 + scripts/src/logseq/tasks/lang.clj | 2 +- src/main/frontend/commands.cljs | 6 +- src/main/frontend/components/block.cljs | 428 +++++++++------- src/main/frontend/components/block.css | 6 + src/main/frontend/components/datetime.cljs | 2 +- src/main/frontend/components/page.cljs | 2 +- src/main/frontend/components/plugins.cljs | 3 +- .../frontend/components/plugins_settings.cljs | 2 +- .../frontend/components/query/builder.cljs | 463 ++++++++++++++++++ .../frontend/components/query/builder.css | 46 ++ src/main/frontend/components/query_table.cljs | 12 +- src/main/frontend/components/select.cljs | 57 ++- src/main/frontend/components/svg.cljs | 2 - src/main/frontend/components/theme.css | 3 +- src/main/frontend/db/model.cljs | 15 +- src/main/frontend/db/query_dsl.cljs | 87 +++- src/main/frontend/db/query_react.cljs | 2 +- src/main/frontend/db/react.cljs | 19 +- src/main/frontend/dicts.cljc | 1 + .../frontend/extensions/video/youtube.cljs | 12 +- .../frontend/handler/command_palette.cljs | 9 +- src/main/frontend/handler/editor.cljs | 16 + src/main/frontend/handler/query/builder.cljs | 197 ++++++++ src/main/frontend/handler/ui.cljs | 4 +- src/main/frontend/modules/shortcut/core.cljs | 2 +- src/main/frontend/search.cljs | 12 +- src/main/frontend/state.cljs | 17 - src/main/frontend/ui.cljs | 13 +- src/main/frontend/ui.css | 4 + src/main/frontend/util.cljc | 7 + src/test/frontend/db/query_dsl_test.cljs | 44 ++ .../frontend/handler/query/builder_test.cljs | 38 ++ 37 files changed, 1269 insertions(+), 301 deletions(-) create mode 100644 src/main/frontend/components/query/builder.cljs create mode 100644 src/main/frontend/components/query/builder.css create mode 100644 src/main/frontend/handler/query/builder.cljs create mode 100644 src/test/frontend/handler/query/builder_test.cljs diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 1b5a625e860..1fd0a5eca1a 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -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 diff --git a/deps/db/src/logseq/db/default.cljs b/deps/db/src/logseq/db/default.cljs index 7535f8f1936..320e690d1fb 100644 --- a/deps/db/src/logseq/db/default.cljs +++ b/deps/db/src/logseq/db/default.cljs @@ -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] diff --git a/deps/db/src/logseq/db/schema.cljs b/deps/db/src/logseq/db/schema.cljs index 0cc280dd21c..d8fde005f7b 100644 --- a/deps/db/src/logseq/db/schema.cljs +++ b/deps/db/src/logseq/db/schema.cljs @@ -118,6 +118,7 @@ :block/properties :block/properties-order :block/properties-text-values + :block/macros :block/invalid-properties :block/created-at :block/updated-at diff --git a/deps/graph-parser/src/logseq/graph_parser/util.cljs b/deps/graph-parser/src/logseq/graph_parser/util.cljs index 295f6f3a818..9f201d8404d 100644 --- a/deps/graph-parser/src/logseq/graph_parser/util.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/util.cljs @@ -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 diff --git a/resources/css/common.css b/resources/css/common.css index 69f5090e5e6..034161bd555 100644 --- a/resources/css/common.css +++ b/resources/css/common.css @@ -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; + } +} diff --git a/scripts/src/logseq/tasks/lang.clj b/scripts/src/logseq/tasks/lang.clj index ca640782f93..fbc6dbb4214 100644 --- a/scripts/src/logseq/tasks/lang.clj +++ b/scripts/src/logseq/tasks/lang.clj @@ -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]}) diff --git a/src/main/frontend/commands.cljs b/src/main/frontend/commands.cljs index 62060c642a0..dcb15781d84 100644 --- a/src/main/frontend/commands.cljs +++ b/src/main/frontend/commands.cljs @@ -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)) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 5295f20fe54..1bc105dd196 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -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))))] - - (symbol? form) - [false (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?))) - -(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))) + _ (reset! *query-error nil) + query-atom (try + (cond + (:dsl-query? config) + (let [q (:query query) + form (safe-read-string q false)] + (cond + ;; Searches like 'foo' or 'foo bar' come back as symbols + ;; and are meant to go directly to full text search + (and (util/electron?) (symbol? form)) ; full-text search + (p/let [blocks (search/block-search repo (string/trim (str form)) {:limit 30})] + (when (seq blocks) + (let [result (db/pull-many (state/get-current-repo) '[*] (map (fn [b] [:block/uuid (uuid (:block/uuid b))]) blocks))] + (reset! result-atom result)))) + + (symbol? form) + (atom nil) + + :else + (query-dsl/query (state/get-current-repo) q))) + + :else + (db/custom-query query {:current-block-uuid current-block-uuid})) + (catch :default e + (reset! *query-error e) + (atom nil)))] + (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")]] - - ;;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 (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.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)))})])))) + (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"))]) + + (when (and current-block (not view-f) (nil? table-view?) (not page-list?)) + (if table? + [:a.flex.ml-1.fade-link {:title "Switch to list view" + :on-click (fn [] (editor-handler/set-block-property! current-block-uuid + "query-table" + false))} + (ui/icon "list" {:style {:font-size 20}})] + [:a.flex.ml-1.fade-link {:title "Switch to table view" + :on-click (fn [] (editor-handler/set-block-property! current-block-uuid + "query-table" + true))} + (ui/icon "table" {:style {:font-size 20}})])) + + [:a.flex.ml-1.fade-link + {:title "Setting properties" + :on-click (fn [] + (let [all-keys (query-table/get-keys result page-list?)] + (state/pub-event! [:modal/set-query-properties current-block all-keys])))} + (ui/icon "settings" {:style {:font-size 20}})] + + [:div.ml-1 + (when (or full-text-search? + (and query-time (> query-time 50))) + (query-refresh-button query-time {:full-text-search? full-text-search? + :on-mouse-down (fn [e] + (util/stop e) + (trigger-custom-query! state *query-error))}))]])]) + (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] diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index 498cee5ceaa..645ae546eee 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -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; diff --git a/src/main/frontend/components/datetime.cljs b/src/main/frontend/components/datetime.cljs index 4ce8db562d2..0c82c95a1a2 100644 --- a/src/main/frontend/components/datetime.cljs +++ b/src/main/frontend/components/datetime.cljs @@ -62,7 +62,7 @@ {:label "w"} {:label "m"} {:label "y"}]) - (fn [value] + (fn [_e value] (swap! *timestamp assoc-in [:repeater :duration] value)) nil) diff --git a/src/main/frontend/components/page.cljs b/src/main/frontend/components/page.cljs index 2cbac41218f..9c0396753ec 100644 --- a/src/main/frontend/components/page.cljs +++ b/src/main/frontend/components/page.cljs @@ -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 diff --git a/src/main/frontend/components/plugins.cljs b/src/main/frontend/components/plugins.cljs index c7619680068..c9ec1bcaaf8 100644 --- a/src/main/frontend/components/plugins.cljs +++ b/src/main/frontend/components/plugins.cljs @@ -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)} diff --git a/src/main/frontend/components/plugins_settings.cljs b/src/main/frontend/components/plugins_settings.cljs index c32efc0c405..b8d70d16979 100644 --- a/src/main/frontend/components/plugins_settings.cljs +++ b/src/main/frontend/components/plugins_settings.cljs @@ -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 diff --git a/src/main/frontend/components/query/builder.cljs b/src/main/frontend/components/query/builder.cljs new file mode 100644 index 00000000000..429c52daf4d --- /dev/null +++ b/src/main/frontend/components/query/builder.cljs @@ -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] [])]])) diff --git a/src/main/frontend/components/query/builder.css b/src/main/frontend/components/query/builder.css new file mode 100644 index 00000000000..a55033e44c3 --- /dev/null +++ b/src/main/frontend/components/query/builder.css @@ -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; + } +} diff --git a/src/main/frontend/components/query_table.cljs b/src/main/frontend/components/query_table.cljs index a7f202df8b2..eea83710d6f 100644 --- a/src/main/frontend/components/query_table.cljs +++ b/src/main/frontend/components/query_table.cljs @@ -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) diff --git a/src/main/frontend/components/select.cljs b/src/main/frontend/components/select.cljs index f064cd8143a..bee8797fac2 100644 --- a/src/main/frontend/components/select.cljs +++ b/src/main/frontend/components/select.cljs @@ -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 diff --git a/src/main/frontend/components/svg.cljs b/src/main/frontend/components/svg.cljs index 035ad34b3d8..7b5c145a5d7 100644 --- a/src/main/frontend/components/svg.cljs +++ b/src/main/frontend/components/svg.cljs @@ -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" diff --git a/src/main/frontend/components/theme.css b/src/main/frontend/components/theme.css index 123667792b7..d9c9bdbe3eb 100644 --- a/src/main/frontend/components/theme.css +++ b/src/main/frontend/components/theme.css @@ -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; } } -} \ No newline at end of file +} diff --git a/src/main/frontend/db/model.cljs b/src/main/frontend/db/model.cljs index 182edeb1a1f..9fba1ba7baf 100644 --- a/src/main/frontend/db/model.cljs +++ b/src/main/frontend/db/model.cljs @@ -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" diff --git a/src/main/frontend/db/query_dsl.cljs b/src/main/frontend/db/query_dsl.cljs index 5f02ad24be9..d5207fea48f 100644 --- a/src/main/frontend/db/query_dsl.cljs +++ b/src/main/frontend/db/query_dsl.cljs @@ -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 diff --git a/src/main/frontend/db/query_react.cljs b/src/main/frontend/db/query_react.cljs index 994b6ab6a83..c31c1e17a60 100644 --- a/src/main/frontend/db/query_react.cljs +++ b/src/main/frontend/db/query_react.cljs @@ -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) diff --git a/src/main/frontend/db/react.cljs b/src/main/frontend/db/react.cljs index ab4b4b0668d..54619e959fd 100644 --- a/src/main/frontend/db/react.cljs +++ b/src/main/frontend/db/react.cljs @@ -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)))))))) diff --git a/src/main/frontend/dicts.cljc b/src/main/frontend/dicts.cljc index f5549ca9c04..45a620967d0 100644 --- a/src/main/frontend/dicts.cljc +++ b/src/main/frontend/dicts.cljc @@ -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" diff --git a/src/main/frontend/extensions/video/youtube.cljs b/src/main/frontend/extensions/video/youtube.cljs index 1eba6742f7c..1779b6580e1 100644 --- a/src/main/frontend/extensions/video/youtube.cljs +++ b/src/main/frontend/extensions/video/youtube.cljs @@ -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))) diff --git a/src/main/frontend/handler/command_palette.cljs b/src/main/frontend/handler/command_palette.cljs index 6223630d073..bd69134ac3f 100644 --- a/src/main/frontend/handler/command_palette.cljs +++ b/src/main/frontend/handler/command_palette.cljs @@ -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] diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index abd1fab6e19..e81850b1ee8 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -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) diff --git a/src/main/frontend/handler/query/builder.cljs b/src/main/frontend/handler/query/builder.cljs new file mode 100644 index 00000000000..9fa6369109b --- /dev/null +++ b/src/main/frontend/handler/query/builder.cljs @@ -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)) diff --git a/src/main/frontend/handler/ui.cljs b/src/main/frontend/handler/ui.cljs index 894030faaed..94a4c0a5e3a 100644 --- a/src/main/frontend/handler/ui.cljs +++ b/src/main/frontend/handler/ui.cljs @@ -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] diff --git a/src/main/frontend/modules/shortcut/core.cljs b/src/main/frontend/modules/shortcut/core.cljs index 0905b9f45df..457c729c592 100644 --- a/src/main/frontend/modules/shortcut/core.cljs +++ b/src/main/frontend/modules/shortcut/core.cljs @@ -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) diff --git a/src/main/frontend/search.cljs b/src/main/frontend/search.cljs index d177e6e3721..4fc9e6ddb4a 100644 --- a/src/main/frontend/search.cljs +++ b/src/main/frontend/search.cljs @@ -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 diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 028f9907716..54b776faad5 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -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)) diff --git a/src/main/frontend/ui.cljs b/src/main/frontend/ui.cljs index 694de0b9f40..f79154a087c 100644 --- a/src/main/frontend/ui.cljs +++ b/src/main/frontend/ui.cljs @@ -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-> diff --git a/src/main/frontend/ui.css b/src/main/frontend/ui.css index 827442e7211..73956e91f7c 100644 --- a/src/main/frontend/ui.css +++ b/src/main/frontend/ui.css @@ -404,3 +404,7 @@ html.is-mobile { content: " "; } } + +.ui__radio-list { + @apply grid grid-flow-col gap-2; +} diff --git a/src/main/frontend/util.cljc b/src/main/frontend/util.cljc index ce7fd9a7425..fc464f31ba3 100644 --- a/src/main/frontend/util.cljc +++ b/src/main/frontend/util.cljc @@ -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))) diff --git a/src/test/frontend/db/query_dsl_test.cljs b/src/test/frontend/db/query_dsl_test.cljs index 9aef50e46b3..80a9cdcd047 100644 --- a/src/test/frontend/db/query_dsl_test.cljs +++ b/src/test/frontend/db/query_dsl_test.cljs @@ -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!) diff --git a/src/test/frontend/handler/query/builder_test.cljs b/src/test/frontend/handler/query/builder_test.cljs new file mode 100644 index 00000000000..686c97ebd7b --- /dev/null +++ b/src/test/frontend/handler/query/builder_test.cljs @@ -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]])))