Browse Source

Merge branch 'feat/db' into refactor/assets

Tienson Qin 1 year ago
parent
commit
998fc66132
82 changed files with 2125 additions and 1511 deletions
  1. 2 0
      .clj-kondo/config.edn
  2. 1 1
      deps.edn
  3. 1 0
      deps/db/.carve/config.edn
  4. 2 33
      deps/db/src/logseq/db.cljs
  5. 4 0
      deps/db/src/logseq/db/frontend/entity_util.cljs
  6. 11 10
      deps/db/src/logseq/db/frontend/property.cljs
  7. 2 1
      deps/db/src/logseq/db/frontend/schema.cljs
  8. 20 0
      deps/db/src/logseq/db/test/helper.cljs
  9. 20 29
      deps/db/test/logseq/db/frontend/inputs_test.cljs
  10. 69 80
      deps/db/test/logseq/db/frontend/rules_test.cljs
  11. 5 9
      deps/db/test/logseq/db/sqlite/build_test.cljs
  12. 6 9
      deps/db/test/logseq/db/sqlite/create_graph_test.cljs
  13. 15 4
      deps/db/test/logseq/db_test.cljs
  14. 1 1
      deps/graph-parser/deps.edn
  15. 67 14
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  16. 42 29
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  17. 3 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md
  18. 5 4
      deps/outliner/src/logseq/outliner/db_pipeline.cljs
  19. 6 13
      deps/outliner/src/logseq/outliner/property.cljs
  20. 43 19
      deps/outliner/src/logseq/outliner/validate.cljs
  21. 21 29
      deps/outliner/test/logseq/outliner/property_test.cljs
  22. 66 14
      deps/outliner/test/logseq/outliner/validate_test.cljs
  23. 14 15
      deps/publishing/src/logseq/publishing/html.cljs
  24. 1 1
      deps/shui/deps.edn
  25. 6 2
      gulpfile.js
  26. 5 2
      package.json
  27. 0 1
      public/index.html
  28. 1 1
      resources/css/codemirror.lsradix.css
  29. 0 1
      resources/index.html
  30. 0 1
      resources/js/prop-types.min.js
  31. 0 4
      resources/js/tabler-icons-react.min.js
  32. 0 0
      resources/js/tabler.min.js
  33. 35 42
      scripts/src/logseq/tasks/db_graph/create_graph_with_properties.cljs
  34. 2 0
      scripts/src/logseq/tasks/dev/db_and_file_graphs.clj
  35. 78 44
      src/main/frontend/commands.cljs
  36. 208 126
      src/main/frontend/components/block.cljs
  37. 25 4
      src/main/frontend/components/block.css
  38. 2 4
      src/main/frontend/components/cmdk/core.cljs
  39. 80 80
      src/main/frontend/components/container.cljs
  40. 5 2
      src/main/frontend/components/editor.cljs
  41. 45 0
      src/main/frontend/components/file_based/block.cljs
  42. 14 12
      src/main/frontend/components/file_based/datetime.cljs
  43. 2 2
      src/main/frontend/components/file_based/query.cljs
  44. 54 50
      src/main/frontend/components/objects.cljs
  45. 16 16
      src/main/frontend/components/page_menu.cljs
  46. 5 14
      src/main/frontend/components/property.cljs
  47. 81 79
      src/main/frontend/components/property/config.cljs
  48. 12 61
      src/main/frontend/components/property/value.cljs
  49. 87 76
      src/main/frontend/components/query.cljs
  50. 23 18
      src/main/frontend/components/query/builder.cljs
  51. 5 1
      src/main/frontend/components/query/builder.css
  52. 40 52
      src/main/frontend/components/query/result.cljs
  53. 1 3
      src/main/frontend/components/query/view.cljs
  54. 12 12
      src/main/frontend/components/select.cljs
  55. 33 32
      src/main/frontend/components/theme.cljs
  56. 4 0
      src/main/frontend/components/theme.css
  57. 7 1
      src/main/frontend/components/views.cljs
  58. 6 6
      src/main/frontend/db/async.cljs
  59. 55 26
      src/main/frontend/db/model.cljs
  60. 15 14
      src/main/frontend/db/query_custom.cljs
  61. 1 1
      src/main/frontend/db/query_dsl.cljs
  62. 62 11
      src/main/frontend/extensions/code.cljs
  63. 1 1
      src/main/frontend/extensions/fsrs.cljs
  64. 20 15
      src/main/frontend/format/block.cljs
  65. 9 6
      src/main/frontend/handler/block.cljs
  66. 1 1
      src/main/frontend/handler/code.cljs
  67. 11 6
      src/main/frontend/handler/db_based/page.cljs
  68. 60 74
      src/main/frontend/handler/editor.cljs
  69. 53 1
      src/main/frontend/handler/events.cljs
  70. 48 0
      src/main/frontend/handler/file_based/editor.cljs
  71. 1 1
      src/main/frontend/handler/file_based/repeated.cljs
  72. 100 96
      src/main/frontend/state.cljs
  73. 9 7
      src/main/frontend/ui.cljs
  74. 6 7
      src/main/frontend/util.cljc
  75. 27 2
      src/main/frontend/worker/db/migrate.cljs
  76. 65 55
      src/main/frontend/worker/handler/page/db_based/page.cljs
  77. 66 63
      src/main/frontend/worker/search.cljs
  78. 38 44
      src/test/frontend/components/query/result_test.cljs
  79. 79 0
      src/test/frontend/worker/handler/page/db_based/page_test.cljs
  80. 1 0
      tailwind.all.css
  81. 0 0
      validate.cljs
  82. 76 15
      yarn.lock

+ 2 - 0
.clj-kondo/config.edn

@@ -173,6 +173,7 @@
              logseq.db.frontend.validate db-validate
              logseq.db.sqlite.cli sqlite-cli
              logseq.db.sqlite.util sqlite-util
+             logseq.db.test.helper db-test
              logseq.graph-parser graph-parser
              logseq.graph-parser.text text
              logseq.graph-parser.db gp-db
@@ -201,6 +202,7 @@
  :lint-as {promesa.core/let clojure.core/let
            promesa.core/loop clojure.core/loop
            promesa.core/recur clojure.core/recur
+           promesa.core/doseq clojure.core/doseq
            rum.core/defcc rum.core/defc
            rum.core/with-context clojure.core/let
            rum.core/defcontext clojure.core/def

+ 1 - 1
deps.edn

@@ -8,7 +8,7 @@
 
   datascript-transit/datascript-transit {:mvn/version "0.3.0"}
   borkdude/rewrite-edn                  {:mvn/version "0.4.7"}
-  funcool/promesa                       {:mvn/version "4.0.2"}
+  funcool/promesa                       {:mvn/version "11.0.678"}
   medley/medley                         {:mvn/version "1.4.0"}
   metosin/reitit-frontend               {:mvn/version "0.3.10"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}

+ 1 - 0
deps/db/.carve/config.edn

@@ -12,5 +12,6 @@
                   ;; Some fns are used by frontend but not worth moving over yet
                   logseq.db.frontend.schema
                   logseq.db.frontend.validate
+                  logseq.db.test.helper
                   logseq.db]
  :report {:format :ignore}}

+ 2 - 33
deps/db/src/logseq/db.cljs

@@ -21,38 +21,6 @@
             [logseq.common.util.namespace :as ns-util])
   (:refer-clojure :exclude [object?]))
 
-;; Use it as an input argument for datalog queries
-(def block-attrs
-  '[:db/id
-    :block/uuid
-    :block/parent
-    :block/order
-    :block/collapsed?
-    :block/format
-    :block/refs
-    :block/_refs
-    :block/path-refs
-    :block/tags
-    :block/link
-    :block/title
-    :block/marker
-    :block/priority
-    :block/properties
-    :block/properties-order
-    :block/properties-text-values
-    :block/pre-block?
-    :block/scheduled
-    :block/deadline
-    :block/repeated?
-    :block/created-at
-    :block/updated-at
-    ;; TODO: remove this in later releases
-    :block/heading-level
-    :block/file
-    :logseq.property/parent
-    {:block/page [:db/id :block/name :block/title :block/journal-day]}
-    {:block/_parent ...}])
-
 (defonce *transact-fn (atom nil))
 (defn register-transact-fn!
   [f]
@@ -108,6 +76,7 @@
              (throw e))))))))
 
 (def page? entity-util/page?)
+(def internal-page? entity-util/internal-page?)
 (def class? entity-util/class?)
 (def property? entity-util/property?)
 (def closed-value? entity-util/closed-value?)
@@ -504,7 +473,7 @@
   [page]
   (cond (property? page)
         (not (public-built-in-property? page))
-        (or (class? page) (= "page" (:block/type page)))
+        (or (class? page) (internal-page? page))
         false
         ;; Default to true for closed value and future internal types.
         ;; Other types like whiteboard are not considered because they aren't built-in

+ 4 - 0
deps/db/src/logseq/db/frontend/entity_util.cljs

@@ -16,6 +16,10 @@
   (contains? #{"page" "journal" "whiteboard" "class" "property" "hidden"}
              (:block/type block)))
 
+(defn internal-page?
+  [entity]
+  (= (:block/type entity) "page"))
+
 (defn class?
   [entity]
   (= (:block/type entity) "class"))

+ 11 - 10
deps/db/src/logseq/db/frontend/property.cljs

@@ -38,12 +38,12 @@
                                    :cardinality :many
                                    :public? true
                                    :classes #{:logseq.class/Root}}}
-   :logseq.property.node/type {:title "Node type"
-                               :schema {:type :keyword
-                                        :public? false
-                                        :hide? true
-                                        :view-context :block}}
-   :logseq.property.code/mode {:title "Code mode"
+   :logseq.property.node/display-type {:title "Node Display Type"
+                                       :schema {:type :keyword
+                                                :public? false
+                                                :hide? true
+                                                :view-context :block}}
+   :logseq.property.code/lang {:title "Code mode"
                                :schema {:type :string
                                         :public? false
                                         :hide? true
@@ -61,10 +61,11 @@
                                           :schema {:type :checkbox
                                                    :public? true
                                                    :view-context :class}}
-   :logseq.property/query {:title "Query"
-                           :schema {:type :default
-                                    :public? true
-                                    :view-context :block}}
+   :logseq.property/query       {:title "Query"
+                                 :schema {:type :default
+                                          :public? true
+                                          :hide? true
+                                          :view-context :block}}
    :logseq.property/page-tags {:title "Page Tags"
                                :schema {:type :page
                                         :public? true

+ 2 - 1
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,8 @@
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
 
-(def version 28)
+(def version 30)
+
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema
   {:db/ident        {:db/unique :db.unique/identity}

+ 20 - 0
deps/db/src/logseq/db/test/helper.cljs

@@ -0,0 +1,20 @@
+(ns ^:node-only logseq.db.test.helper
+  "Main ns for providing test fns for DB graphs"
+  (:require [datascript.core :as d]
+            [logseq.db.sqlite.build :as sqlite-build]
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
+            [logseq.db.frontend.schema :as db-schema]))
+
+(defn create-conn
+  "Create a conn for a DB graph seeded with initial data"
+  []
+  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
+        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))]
+    conn))
+
+(defn create-conn-with-blocks
+  "Create a conn with create-db-conn and then create blocks using sqlite-build"
+  [opts]
+  (let [conn (create-conn)
+        _ (sqlite-build/create-blocks conn opts)]
+    conn))

+ 20 - 29
deps/db/test/logseq/db/frontend/inputs_test.cljs

@@ -7,7 +7,7 @@
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.inputs :as db-inputs]
             [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]))
+            [logseq.db.test.helper :as db-test]))
 
 (defn- custom-query [db {:keys [inputs query input-options]}]
   (let [q-args (cond-> (mapv #(db-inputs/resolve-input db % input-options) inputs)
@@ -97,14 +97,11 @@
         ":parent-block input resolves to parent of current blocks's :db/id")))
 
 (deftest resolve-input-for-journal-date-inputs
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
-        _ (sqlite-build/create-blocks
-           conn
-           [{:page {:build/journal 20230101}
-             :blocks [{:block/title "b1"}]}
-            {:page {:build/journal 20230107}
-             :blocks [{:block/title "b2"}]}])]
+  (let [conn (db-test/create-conn-with-blocks
+              [{:page {:build/journal 20230101}
+                :blocks [{:block/title "b1"}]}
+               {:page {:build/journal 20230107}
+                :blocks [{:block/title "b2"}]}])]
     (is (= ["b2"]
            (with-redefs [t/today (constantly (t/date-time 2023 1 7))]
              (map :block/title
@@ -148,13 +145,10 @@
 
 (deftest resolve-input-for-query-page
   (let [current-date (t/date-time 2023 1 1)
-        conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
-        _ (sqlite-build/create-blocks
-           conn
-           [{:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
-            {:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
-            {:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}])
+        conn (db-test/create-conn-with-blocks
+              [{:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
+               {:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
+               {:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}])
         db @conn]
     (is (= ["now"] (blocks-on-journal-page-from-block-with-content db :current-page "now" current-date))
         ":current-page resolves to the stateful page when called from a block on the stateful page")
@@ -179,19 +173,16 @@
                                 :where (between ?b ?start ?end)]}))))
 
 (deftest resolve-input-for-relative-date-queries
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
-        _ (sqlite-build/create-blocks
-           conn
-           [{:page {:build/journal 20220101} :blocks [{:block/title "-1y"}]}
-            {:page {:build/journal 20221201} :blocks [{:block/title "-1m"}]}
-            {:page {:build/journal 20221225} :blocks [{:block/title "-1w"}]}
-            {:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
-            {:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
-            {:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}
-            {:page {:build/journal 20230108} :blocks [{:block/title "+1w"}]}
-            {:page {:build/journal 20230201} :blocks [{:block/title "+1m"}]}
-            {:page {:build/journal 20240101} :blocks [{:block/title "+1y"}]}])
+  (let [conn (db-test/create-conn-with-blocks
+              [{:page {:build/journal 20220101} :blocks [{:block/title "-1y"}]}
+               {:page {:build/journal 20221201} :blocks [{:block/title "-1m"}]}
+               {:page {:build/journal 20221225} :blocks [{:block/title "-1w"}]}
+               {:page {:build/journal 20221231} :blocks [{:block/title "-1d"}]}
+               {:page {:build/journal 20230101} :blocks [{:block/title "now"}]}
+               {:page {:build/journal 20230102} :blocks [{:block/title "+1d"}]}
+               {:page {:build/journal 20230108} :blocks [{:block/title "+1w"}]}
+               {:page {:build/journal 20230201} :blocks [{:block/title "+1m"}]}
+               {:page {:build/journal 20240101} :blocks [{:block/title "+1y"}]}])
         db @conn]
     (with-redefs [t/today (constantly (t/date-time 2023 1 1))]
       (is (= ["now" "-1d" "-1w" "-1m" "-1y"] (blocks-journaled-between-inputs db :-365d :today))

+ 69 - 80
deps/db/test/logseq/db/frontend/rules_test.cljs

@@ -1,15 +1,8 @@
 (ns logseq.db.frontend.rules-test
   (:require [cljs.test :refer [deftest is testing]]
             [datascript.core :as d]
-            [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.rules :as rules]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
-            [logseq.db.sqlite.build :as sqlite-build]))
-
-(defn- new-db-conn []
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))]
-    conn))
+            [logseq.db.test.helper :as db-test]))
 
 (defn q-with-rules [query db]
   ;; query assumes no :in given
@@ -18,14 +11,12 @@
        (rules/extract-rules rules/db-query-dsl-rules)))
 
 (deftest has-page-property-rule
-  (let [conn (new-db-conn)
-        _ (sqlite-build/create-blocks
-           conn
-           {:properties {:foo {:block/schema {:type :default}}
-                         :foo2 {:block/schema {:type :default}}}
-            :pages-and-blocks
-            [{:page {:block/title "Page"
-                     :build/properties {:foo "bar"}}}]})]
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:foo {:block/schema {:type :default}}
+                            :foo2 {:block/schema {:type :default}}}
+               :pages-and-blocks
+               [{:page {:block/title "Page"
+                        :build/properties {:foo "bar"}}}]})]
 
     (is (= ["Page"]
            (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-page-property ?b :user.property/foo)]
@@ -44,35 +35,33 @@
         "has-page-property can bind to property arg")))
 
 (deftest page-property-rule
-  (let [conn (new-db-conn)
-        _ (sqlite-build/create-blocks
-           conn
-           {:properties {:foo {:block/schema {:type :default}}
-                         :foo2 {:block/schema {:type :default}}
-                         :number-many {:block/schema {:type :number :cardinality :many}}
-                         :page-many {:block/schema {:type :node :cardinality :many}}}
-            :pages-and-blocks
-            [{:page {:block/title "Page"
-                     :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}}
-             {:page {:block/title "Page A"
-                     :build/properties {:foo "bar A"}}}]})]
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:foo {:block/schema {:type :default}}
+                            :foo2 {:block/schema {:type :default}}
+                            :number-many {:block/schema {:type :number :cardinality :many}}
+                            :page-many {:block/schema {:type :node :cardinality :many}}}
+               :pages-and-blocks
+               [{:page {:block/title "Page"
+                        :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}}
+                {:page {:block/title "Page A"
+                        :build/properties {:foo "bar A"}}}]})]
     (testing "cardinality :one property"
-        (is (= ["Page"]
-               (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "bar")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns result when page has property")
-        (is (= []
-               (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "baz")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns no result when page doesn't have property value")
-        (is (= #{:user.property/foo}
-               (->> (q-with-rules '[:find [?p ...]
-                                    :where (page-property ?b ?p "bar") [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property arg with bound property value"))
+      (is (= ["Page"]
+             (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "bar")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns result when page has property")
+      (is (= []
+             (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (page-property ?b :user.property/foo "baz")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns no result when page doesn't have property value")
+      (is (= #{:user.property/foo}
+             (->> (q-with-rules '[:find [?p ...]
+                                  :where (page-property ?b ?p "bar") [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property arg with bound property value"))
 
     (testing "cardinality :many property"
       (is (= ["Page"]
@@ -94,41 +83,41 @@
 
     ;; NOTE: Querying a ref's name is different than before and requires more than just the rule
     (testing ":ref property"
-        (is (= ["Page"]
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where (page-property ?b :user.property/page-many "Page A")]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns result when page has property")
-        (is (= []
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
-                                  @conn)
-                    (map (comp :block/title first))))
-            "page-property returns no result when page doesn't have property value"))
+      (is (= ["Page"]
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where (page-property ?b :user.property/page-many "Page A")]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns result when page has property")
+      (is (= []
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where [?b :user.property/page-many ?pv] [?pv :block/title "Page B"]]
+                                @conn)
+                  (map (comp :block/title first))))
+          "page-property returns no result when page doesn't have property value"))
 
     (testing "bindings with property value"
-        (is (= #{:user.property/foo :user.property/number-many :user.property/page-many}
-               (->> (q-with-rules '[:find [?p ...]
-                                    :where (page-property ?b ?p _) [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property arg with unbound property value")
-        (is (= #{[:user.property/number-many 10]
-                 [:user.property/number-many 5]
-                 [:user.property/foo "bar"]
-                 [:user.property/page-many "Page A"]}
-               (->> (q-with-rules '[:find ?p ?v
-                                    :where (page-property ?b ?p ?v) [?b :block/title "Page"]]
-                                  @conn)
-                    set))
-            "page-property can bind to property and property value args")
-        (is (= #{"Page"}
-               (->> (q-with-rules '[:find (pull ?b [:block/title])
-                                    :where
-                                    [?b :user.property/page-many ?pv]
-                                    (page-property ?pv :user.property/foo "bar A")]
-                                  @conn)
-                    (map (comp :block/title first))
-                    set))
-            "page-property can be used multiple times to query a property value's property"))))
+      (is (= #{:user.property/foo :user.property/number-many :user.property/page-many}
+             (->> (q-with-rules '[:find [?p ...]
+                                  :where (page-property ?b ?p _) [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property arg with unbound property value")
+      (is (= #{[:user.property/number-many 10]
+               [:user.property/number-many 5]
+               [:user.property/foo "bar"]
+               [:user.property/page-many "Page A"]}
+             (->> (q-with-rules '[:find ?p ?v
+                                  :where (page-property ?b ?p ?v) [?b :block/title "Page"]]
+                                @conn)
+                  set))
+          "page-property can bind to property and property value args")
+      (is (= #{"Page"}
+             (->> (q-with-rules '[:find (pull ?b [:block/title])
+                                  :where
+                                  [?b :user.property/page-many ?pv]
+                                  (page-property ?pv :user.property/foo "bar A")]
+                                @conn)
+                  (map (comp :block/title first))
+                  set))
+          "page-property can be used multiple times to query a property value's property"))))

+ 5 - 9
deps/db/test/logseq/db/sqlite/build_test.cljs

@@ -1,14 +1,12 @@
 (ns logseq.db.sqlite.build-test
   (:require [cljs.test :refer [deftest is]]
             [datascript.core :as d]
-            [logseq.db.frontend.schema :as db-schema]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.test.helper :as db-test]))
 
 (deftest build-tags
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         _ (sqlite-build/create-blocks
            conn
            [{:page {:block/title "page1"}
@@ -27,8 +25,7 @@
         "Person class is created and correctly associated to a page")))
 
 (deftest build-properties-user
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         _ (sqlite-build/create-blocks
            conn
            [{:page {:block/title "page1"}
@@ -53,8 +50,7 @@
         "description property is created and correctly associated to a page")))
 
 (deftest build-properties-built-in
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         _ (sqlite-build/create-blocks
            conn
            [{:page {:block/title "page1"}

+ 6 - 9
deps/db/test/logseq/db/sqlite/create_graph_test.cljs

@@ -8,12 +8,12 @@
             [logseq.db.frontend.validate :as db-validate]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [logseq.db.test.helper :as db-test]))
 
 (deftest new-graph-db-idents
   (testing "a new graph follows :db/ident conventions for"
-    (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+    (let [conn (db-test/create-conn)
           ident-ents (->> (d/q '[:find (pull ?b [:db/ident :block/type])
                                  :where [?b :db/ident]]
                                @conn)
@@ -45,8 +45,7 @@
               "All closed values start with a prefix that is a property name"))))))
 
 (deftest new-graph-marks-built-ins
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         idents (->> (d/q '[:find [(pull ?b [:db/ident :logseq.property/built-in?]) ...]
                            :where [?b :db/ident]]
                          @conn)
@@ -58,8 +57,7 @@
         "All entities with :db/ident have built-in property (except for kv idents)")))
 
 (deftest new-graph-creates-class
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         task (d/entity @conn :logseq.class/Task)]
     (is (ldb/class? task)
         "Task class has correct type")
@@ -69,8 +67,7 @@
         "Each task property has correct type")))
 
 (deftest new-graph-is-valid
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)
         validation (db-validate/validate-db! @conn)]
     ;; For debugging
     ;; (println (count (:errors validation)) "errors of" (count (:entities validation)))

+ 15 - 4
deps/db/test/logseq/db_test.cljs

@@ -3,8 +3,7 @@
             [logseq.db.frontend.schema :as db-schema]
             [datascript.core :as d]
             [logseq.db :as ldb]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]))
-
+            [logseq.db.test.helper :as db-test]))
 
 ;;; datoms
 ;;; - 1 <----+
@@ -46,10 +45,22 @@
     :logseq.property/parent [:block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"]}])
 
 (deftest get-page-parents
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)]
-    (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+  (let [conn (db-test/create-conn)]
     (d/transact! conn class-parents-data)
     (is (= #{"x" "y"}
            (->> (ldb/get-page-parents (ldb/get-page @conn "z") {:node-class? true})
                 (map :block/title)
                 set)))))
+
+(deftest get-case-page
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties
+               {:foo {:block/schema {:type :default}}
+                :Foo {:block/schema {:type :default}}}
+               :classes {:movie {} :Movie {}}})]
+    ;; Case sensitive properties
+    (is (= "foo" (:block/title (ldb/get-case-page @conn "foo"))))
+    (is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo"))))
+    ;; Case sensitive classes
+    (is (= "movie" (:block/title (ldb/get-case-page @conn "movie"))))
+    (is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie"))))))

+ 1 - 1
deps/graph-parser/deps.edn

@@ -9,7 +9,7 @@
   ;; stubbed in nbb
   com.lambdaisland/glogi      {:mvn/version "1.1.144"}
   ;; built in to nbb
-  funcool/promesa             {:mvn/version "4.0.2"}
+  funcool/promesa             {:mvn/version "11.0.678"}
   cljs-bean/cljs-bean         {:mvn/version "1.5.0"}}
 
  :aliases

+ 67 - 14
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -82,14 +82,18 @@
       (throw (ex-info (str "No uuid found for page name " (pr-str page-name))
                       {:page-name page-name}))))
 
+(defn- logseq-class-ident?
+  [k]
+  (and (qualified-keyword? k) (= "logseq.class" (namespace k))))
+
 (defn- update-page-tags
   [block db tag-classes page-names-to-uuids all-idents]
   (if (seq (:block/tags block))
     (let [page-tags (->> (:block/tags block)
                          (remove #(or (:block.temp/new-class %)
                                       (contains? tag-classes (:block/name %))
-                                      ;; Ignore new class tags from extract
-                                      (= % :logseq.class/Journal)))
+                                      ;; Ignore new class tags from extract e.g. :logseq.class/Journal
+                                      (logseq-class-ident? %)))
                          (map #(vector :block/uuid (get-page-uuid page-names-to-uuids (:block/name %))))
                          set)]
       (cond-> block
@@ -97,7 +101,7 @@
         (update :block/tags
                 (fn [tags]
                   ;; Don't lazy load as this needs to build before the page does
-                  (vec (keep #(if (= % :logseq.class/Journal)
+                  (vec (keep #(if (logseq-class-ident? %)
                                 %
                                 (convert-tag-to-class db % page-names-to-uuids tag-classes all-idents)) tags))))
         (seq page-tags)
@@ -124,7 +128,10 @@
   [block db tag-classes page-names-to-uuids all-idents]
   (let [block'
         (if (seq (:block/tags block))
-          (let [original-tags (remove :block.temp/new-class (:block/tags block))]
+          (let [original-tags (remove #(or (:block.temp/new-class %)
+                                           ;; Filter out new classes already set on a block e.g. :logseq.class/Query
+                                           (logseq-class-ident? %))
+                                      (:block/tags block))]
             (-> block
                 (update :block/title
                         content-without-tags-ignore-case
@@ -138,11 +145,12 @@
                              (map #(add-uuid-to-page-map % page-names-to-uuids))))
                 (update :block/tags
                         (fn [tags]
-                          (vec (keep #(convert-tag-to-class db % page-names-to-uuids tag-classes all-idents) tags))))))
+                          (vec (keep #(if (logseq-class-ident? %)
+                                        %
+                                        (convert-tag-to-class db % page-names-to-uuids tag-classes all-idents))
+                                     tags))))))
           block)]
-    (cond-> block'
-      (macro-util/query-macro? (:block/title block))
-      (update :block/tags (fnil conj []) :logseq.class/Query))))
+    block'))
 
 (defn- update-block-marker
   "If a block has a marker, convert it to a task object"
@@ -614,14 +622,59 @@
 
 (defn- handle-block-properties
   "Does everything page properties does and updates a couple of block specific attributes"
-  [block* db page-names-to-uuids refs {:keys [property-classes] :as options}]
-  (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)]
+  [{:block/keys [title] :as block*} db page-names-to-uuids refs {:keys [property-classes] :as options}]
+  (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
+        advanced-query (some->> (second (re-find #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" title)) string/trim)
+        additional-props (cond-> {}
+                           ;; Order matters as we ensure a simple query gets priority
+                           (macro-util/query-macro? title)
+                           (assoc :logseq.property/query
+                                  (or (some->> (second (re-find #"\{\{query(.*)\}\}" title))
+                                               string/trim)
+                                      title))
+                           (seq advanced-query)
+                           (assoc :logseq.property/query
+                                  (if-let [query-map (not-empty (common-util/safe-read-map-string advanced-query))]
+                                    (pr-str (dissoc query-map :title :group-by-page? :collapsed?))
+                                    advanced-query)))
+        {:keys [block-properties pvalues-tx]}
+        (when (seq additional-props)
+          (build-properties-and-values additional-props db page-names-to-uuids
+                                       (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+                                       options))
+        pvalues-tx' (if (and pvalues-tx (seq advanced-query))
+                      (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
+                                           :logseq.property.code/lang "clojure"
+                                           :logseq.property.node/display-type :code}])
+                      pvalues-tx)]
     {:block
      (cond-> block
+       (seq block-properties)
+       (merge block-properties)
+
+       (macro-util/query-macro? title)
+       ((fn [b]
+          (merge (update b :block/tags (fnil conj []) :logseq.class/Query)
+                 ;; Put all non-query content in title. Could just be a blank string
+                 {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))})))
+
+       (seq advanced-query)
+       ((fn [b]
+          (let [query-map (common-util/safe-read-map-string advanced-query)]
+            (cond-> (update b :block/tags (fnil conj []) :logseq.class/Query)
+              true
+              (assoc :block/title
+                     (or (when-let [title' (:title query-map)]
+                           (if (string? title') title' (pr-str title')))
+                         ;; Put all non-query content in title for now
+                         (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
+              (:collapsed? query-map)
+              (assoc :block/collapsed? true)))))
+
        (and (seq property-classes) (seq (:block/refs block*)))
        ;; remove unused, nonexistent property page
        (update :block/refs (fn [refs] (remove #(property-classes (keyword (:block/name %))) refs))))
-     :properties-tx properties-tx}))
+     :properties-tx (concat properties-tx (when pvalues-tx' pvalues-tx'))}))
 
 (defn- update-block-refs
   "Updates the attributes of a block ref as this is where a new page is defined. Also
@@ -675,7 +728,7 @@
 
 (defn- build-block-tx
   [db block* pre-blocks page-names-to-uuids {:keys [tag-classes import-state] :as options}]
-  ;; (prn ::block-in block)
+  ;; (prn ::block-in block*)
   (let [;; needs to come before update-block-refs to detect new property schemas
         {:keys [block properties-tx]}
         (handle-block-properties block* db page-names-to-uuids (:block/refs block*) options)
@@ -760,7 +813,7 @@
                                                                 m))
                                  block-changes (cond-> (select-keys m allowed-attributes)
                                                  ;; disallow any type -> "page" but do allow any conversion to a non-page type
-                                                 (= (:block/type m) "page")
+                                                 (ldb/internal-page? m)
                                                  (dissoc :block/type))]
                              (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))]
                                (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
@@ -1242,4 +1295,4 @@
        (export-favorites-from-config-edn conn repo-or-conn config {})
        (export-class-properties conn repo-or-conn)
        {:import-state (:import-state doc-options)
-        :files files}))))
+        :files files}))))

+ 42 - 29
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -9,16 +9,16 @@
             ["fs" :as fs]
             [logseq.common.graph :as common-graph]
             [promesa.core :as p]
-            [logseq.db.frontend.schema :as db-schema]
             [logseq.db.frontend.validate :as db-validate]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.db.frontend.malli-schema :as db-malli-schema]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.common.config :as common-config]
             [logseq.db :as ldb]
-            [logseq.outliner.db-pipeline :as db-pipeline]))
+            [logseq.outliner.db-pipeline :as db-pipeline]
+            [logseq.db.test.helper :as db-test]
+            [logseq.db.frontend.rules :as rules]))
 
 ;; Helpers
 ;; =======
@@ -41,6 +41,13 @@
               db)
          first)))
 
+(defn- find-block-by-property [db property property-value]
+  (->> (d/q '[:find [(pull ?b [*]) ...]
+              :in $ ?prop ?prop-value %
+              :where (property ?b ?prop ?prop-value)]
+            db property property-value (rules/extract-rules rules/db-query-dsl-rules [:property]))
+       first))
+
 (defn- find-page-by-name [db name]
   (->> name
        (d/q '[:find [(pull ?b [*]) ...]
@@ -125,13 +132,13 @@
               (if (boolean? v)
                 [k v]
                 [k
-                (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
-                  (if (= :block/tags k)
-                    (mapv #(:db/ident (d/entity db (:db/id %))) v)
-                    (if (db-property-type/all-ref-property-types built-in-type)
-                      (db-property/ref->property-value-contents db v)
-                      v))
-                  (db-property/ref->property-value-contents db v))])))
+                 (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
+                   (if (= :block/tags k)
+                     (mapv #(:db/ident (d/entity db (:db/id %))) v)
+                     (if (db-property-type/all-ref-property-types built-in-type)
+                       (db-property/ref->property-value-contents db v)
+                       v))
+                   (db-property/ref->property-value-contents db v))])))
        (into {})))
 
 ;; Tests
@@ -140,8 +147,7 @@
 (deftest-async ^:integration export-docs-graph
   (p/let [file-graph-dir "test/resources/docs-0.10.9"
           _ (docs-graph-helper/clone-docs-repo-if-not-exists file-graph-dir "v0.10.9")
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           assets (atom [])
           {:keys [import-state]}
           (import-file-graph-to-db file-graph-dir conn {:assets assets})]
@@ -153,8 +159,7 @@
 (deftest-async export-basic-graph
   ;; This graph will contain basic examples of different features to import
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           ;; Simulate frontend path-refs being calculated
           _ (db-pipeline/add-listener conn)
           assets (atom [])
@@ -171,13 +176,13 @@
       (is (= 18 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
-      (is (= 1 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
+      (is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
 
       ;; Don't count pages like url.md that have properties but no content
       (is (= 8
              (count (->> (d/q '[:find [(pull ?b [:block/title :block/type]) ...]
                                 :where [?b :block/title] [_ :block/page ?b]] @conn)
-                         (filter #(= "page" (:block/type %))))))
+                         (filter ldb/internal-page?))))
           "Correct number of pages with block content")
       (is (= 4 (count (d/datoms @conn :avet :block/type "whiteboard"))))
       (is (= 1 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
@@ -289,12 +294,26 @@
       (is (= #{"gpt"}
              (:block/alias (readable-properties @conn (find-page-by-name @conn "chat-gpt")))))
 
+      ;; Queries
       (is (= {:logseq.property.table/sorting [{:id :user.property/prop-num, :asc? false}]
               :logseq.property.view/type "Table View"
               :logseq.property.table/ordered-columns [:block/title :user.property/prop-string :user.property/prop-num]
+              :logseq.property/query "(property :prop-string)"
               :block/tags [:logseq.class/Query]}
-             (readable-properties @conn (find-block-by-content @conn "{{query (property :prop-string)}}")))
-          "query block has correct query properties"))
+             (readable-properties @conn (find-block-by-property @conn :logseq.property/query "(property :prop-string)")))
+          "simple query block has correct query properties")
+      (is (= "For example, here's a query with title text:"
+             (:block/title (find-block-by-content @conn #"query with title text")))
+          "Text around a simple query block is set as a query's title")
+      (is (= {:logseq.property.view/type "List View"
+              :logseq.property/query "{:query (task todo doing)}"
+              :block/tags [:logseq.class/Query]
+              :logseq.property.table/ordered-columns [:block/title]}
+             (readable-properties @conn (find-block-by-content @conn #"tasks with")))
+          "Advanced query has correct query properties")
+      (is (= "tasks with todo and doing"
+             (:block/title (find-block-by-content @conn #"tasks with")))
+          "Advanced query has custom title migrated"))
 
     (testing "db attributes"
       (is (= true
@@ -362,9 +381,7 @@
     (testing "multiline blocks"
       (is (= "|markdown| table|\n|some|thing|" (:block/title (find-block-by-content @conn #"markdown.*table"))))
       (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (find-block-by-content @conn #"multiline block"))))
-      (is (= "logbook block" (:block/title (find-block-by-content @conn #"logbook block"))))
-      (is (re-find #"(?s)^Text before\n#\+BEGIN_QUERY.*END_QUERY\nText after$"
-                   (:block/title (find-block-by-content @conn #":title \"tasks")))))
+      (is (= "logbook block" (:block/title (find-block-by-content @conn #"logbook block")))))
 
     (testing "block refs and path-refs"
       (let [block (find-block-by-content @conn "old todo block")]
@@ -410,8 +427,7 @@
 (deftest-async export-files-with-tag-classes-option
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           _ (import-files-to-db files conn {:tag-classes ["movie"]})]
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
@@ -438,8 +454,7 @@
 (deftest-async export-files-with-property-classes-option
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_23.md" "pages/url.md"])
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           _ (import-files-to-db files conn {:property-classes ["type"]})
           _ (@#'gp-exporter/export-class-properties conn conn)]
 
@@ -483,8 +498,7 @@
 (deftest-async export-files-with-ignored-properties
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           files (mapv #(node-path/join file-graph-dir %) ["ignored/icon-page.md"])
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           {:keys [import-state]} (import-files-to-db files conn {})]
     (is (= 2
            (count (filter #(= :icon (:property %)) @(:ignored-properties import-state))))
@@ -493,8 +507,7 @@
 (deftest-async export-files-with-property-parent-classes-option
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           files (mapv #(node-path/join file-graph-dir %) ["pages/CreativeWork.md" "pages/Movie.md" "pages/type.md"])
-          conn (d/create-conn db-schema/schema-for-db-based-graph)
-          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          conn (db-test/create-conn)
           _ (import-files-to-db files conn {:property-parent-classes ["parent"]})]
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))

+ 3 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md

@@ -1,3 +1,5 @@
+- For example, here's a query with title text:
+{{query (property type book)}}
 - test multilines in this page
 - |markdown| table|
   |some|thing|
@@ -18,4 +20,4 @@
   {:title "tasks with todo and doing"
   :query (task todo doing)}
   #+END_QUERY
-  Text after
+  Text after

+ 5 - 4
deps/outliner/src/logseq/outliner/db_pipeline.cljs

@@ -7,7 +7,8 @@
    * Delete empty property parent"
   (:require [datascript.core :as d]
             [logseq.outliner.datascript-report :as ds-report]
-            [logseq.outliner.pipeline :as outliner-pipeline]))
+            [logseq.outliner.pipeline :as outliner-pipeline]
+            [logseq.db :as ldb]))
 
 (defn- rebuild-block-refs
   [{:keys [db-after]} blocks]
@@ -27,16 +28,16 @@
   (when (not (get-in tx-report [:tx-meta :pipeline-replace?]))
     (let [{:keys [blocks]} (ds-report/get-blocks-and-pages tx-report)
           refs-tx-report (when-let [refs-tx (and (seq blocks) (rebuild-block-refs tx-report blocks))]
-                           (d/transact! conn refs-tx {:pipeline-replace? true}))
+                           (ldb/transact! conn refs-tx {:pipeline-replace? true}))
           blocks' (if refs-tx-report
                     (keep (fn [b] (d/entity (:db-after refs-tx-report) (:db/id b))) blocks)
                     blocks)
           block-path-refs-tx (distinct (outliner-pipeline/compute-block-path-refs-tx tx-report blocks'))]
       (when (seq block-path-refs-tx)
-        (d/transact! conn block-path-refs-tx {:pipeline-replace? true})))))
+        (ldb/transact! conn block-path-refs-tx {:pipeline-replace? true})))))
 
 (defn ^:api add-listener
   "Adds a listener to the datascript connection to add additional changes from outliner.pipeline"
   [conn]
   (d/listen! conn :pipeline-updates (fn pipeline-updates [tx-report]
-                                      (invoke-hooks conn tx-report))))
+                                      (invoke-hooks conn tx-report))))

+ 6 - 13
deps/outliner/src/logseq/outliner/property.cljs

@@ -25,15 +25,6 @@
     (throw (ex-info "Read-only property value shouldn't be edited"
                     {:property property-ident}))))
 
-(defn- throw-error-if-add-class-parent-to-page
-  [blocks entity]
-  (when (and (ldb/class? entity) (not (every? ldb/class? blocks)))
-    (throw (ex-info "Can't set a tag as a parent for non-tag page"
-                    {:type :notification
-                     :payload {:message "Can't set a tag as a parent for non-tag page"
-                               :type :warning}
-                     :blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? blocks))}))))
-
 (defn- build-property-value-tx-data
   ([block property-id value]
    (build-property-value-tx-data block property-id value (= property-id :logseq.task/status)))
@@ -230,7 +221,9 @@
 (defn- find-or-create-property-value
   "Find or create a property value. Only to be used with properties that have ref types"
   [conn property-id v]
-  (or (get-property-value-eid @conn property-id v)
+  ;; FIXME: some properties should always create new values
+  (or (when-not (contains? #{:logseq.property/query} property-id)
+        (get-property-value-eid @conn property-id v))
       (let [v-uuid (create-property-text-block! conn nil property-id v {})]
         (:db/id (d/entity @conn [:block/uuid v-uuid])))))
 
@@ -282,9 +275,9 @@
   (let [block-eids (map ->eid block-ids)
         property (d/entity @conn property-id)
         _ (when (= (:db/ident property) :logseq.property/parent)
-            (throw-error-if-add-class-parent-to-page
-             (map #(d/entity @conn %) block-eids)
-             (if (number? v) (d/entity @conn v) v)))
+            (outliner-validate/validate-parent-property
+             (if (number? v) (d/entity @conn v) v)
+             (map #(d/entity @conn %) block-eids)))
         _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
         property-type (get-in property [:block/schema :type] :default)
         _ (assert (some? v) "Can't set a nil property value must be not nil")

+ 43 - 19
deps/outliner/src/logseq/outliner/validate.cljs

@@ -41,10 +41,34 @@
                      :payload {:message "Built-in pages can't be edited"
                                :type :warning}}))))
 
+(defn- validate-unique-for-property-page
+  [entity db new-title]
+  (when-let [_res (seq (d/q (if (:logseq.property/built-in? entity)
+                              '[:find [?b ...]
+                                :in $ ?eid ?title
+                                :where
+                                [?b :block/title ?title]
+                                [?b :block/type "property"]
+                                [(not= ?b ?eid)]]
+                              '[:find [?b ...]
+                                :in $ ?eid ?title
+                                :where
+                                [?b :block/title ?title]
+                                [?b :block/type "property"]
+                                [(missing? $ ?b :logseq.property/built-in?)]
+                                [(not= ?b ?eid)]])
+                            db
+                            (:db/id entity)
+                            new-title))]
+    (throw (ex-info "Duplicate property"
+                    {:type :notification
+                     :payload {:message (str "Another property named " (pr-str new-title) " already exists")
+                               :type :warning}}))))
+
 (defn- validate-unique-for-page
   [db new-title {:block/keys [tags] :as entity}]
   (cond
-    (and (seq tags) (= "page" (:block/type entity)))
+    (and (seq tags) (ldb/internal-page? entity))
     (when-let [res (seq (d/q '[:find [?b ...]
                                :in $ ?eid ?title [?tag-id ...]
                                :where
@@ -62,21 +86,7 @@
                                  :type :warning}})))
 
     (ldb/property? entity)
-    (when-let [_res (seq (d/q '[:find [?b ...]
-                                :in $ ?eid ?type ?title
-                                :where
-                                [?b :block/title ?title]
-                                [?b :block/type ?type]
-                                [(missing? $ ?b :logseq.property/built-in?)]
-                                [(not= ?b ?eid)]]
-                              db
-                              (:db/id entity)
-                              (:block/type entity)
-                              new-title))]
-      (throw (ex-info "Duplicate property"
-                      {:type :notification
-                       :payload {:message (str "Another property named " (pr-str new-title) " already exists")
-                                 :type :warning}})))
+    (validate-unique-for-property-page entity db new-title)
 
     :else
     (when-let [_res (seq (d/q '[:find [?b ...]
@@ -109,9 +119,9 @@
   (when (and (ldb/page? entity) (not (ldb/journal? entity))
              (common-date/normalize-date new-title nil))
     (throw (ex-info "Page can't be renamed to a journal"
-                      {:type :notification
-                       :payload {:message "This page can't be changed to a journal page"
-                                 :type :warning}}))))
+                    {:type :notification
+                     :payload {:message "This page can't be changed to a journal page"
+                               :type :warning}}))))
 
 (defn validate-block-title
   "Validates a block title when it has changed"
@@ -119,3 +129,17 @@
   (validate-built-in-pages existing-block-entity)
   (validate-unique-by-name-tag-and-block-type db new-title existing-block-entity)
   (validate-disallow-page-with-journal-name new-title existing-block-entity))
+
+(defn validate-parent-property
+  "Validates whether given parent and children are valid. Allows 'class' and
+  'page' types to have a relationship with their own type. May consider allowing more
+  page types if they don't cause systemic bugs"
+  [parent-ent child-ents]
+  (when (or (and (ldb/class? parent-ent) (not (every? ldb/class? child-ents)))
+            (and (ldb/internal-page? parent-ent) (not (every? ldb/internal-page? child-ents)))
+            (not ((some-fn ldb/class? ldb/internal-page?) parent-ent)))
+    (throw (ex-info "Can't set this page as a parent because the child page is a different type"
+                    {:type :notification
+                     :payload {:message "Can't set this page as a parent because the child page is a different type"
+                               :type :warning}
+                     :blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? child-ents))}))))

+ 21 - 29
deps/outliner/test/logseq/outliner/property_test.cljs

@@ -1,11 +1,9 @@
 (ns logseq.outliner.property-test
   (:require [cljs.test :refer [deftest is testing are]]
-            [logseq.db.frontend.schema :as db-schema]
             [datascript.core :as d]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
-            [logseq.db.sqlite.build :as sqlite-build]
             [logseq.outliner.property :as outliner-property]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.test.helper :as db-test]))
 
 (defn- find-block-by-content [conn content]
   (->> content
@@ -15,27 +13,21 @@
             @conn)
        first))
 
-(defn- create-conn-with-blocks [opts]
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
-        _ (sqlite-build/create-blocks conn opts)]
-    conn))
-
 (deftest upsert-property!
   (testing "Creates a property"
-    (let [conn (create-conn-with-blocks [])
+    (let [conn (db-test/create-conn-with-blocks [])
           _ (outliner-property/upsert-property! conn nil {:type :number} {:property-name "num"})]
       (is (= {:type :number}
              (:block/schema (d/entity @conn :user.property/num)))
           "Creates property with property-name")))
 
   (testing "Updates a property"
-    (let [conn (create-conn-with-blocks {:properties {:num {:block/schema {:type :number}}}})
+    (let [conn (db-test/create-conn-with-blocks {:properties {:num {:block/schema {:type :number}}}})
           _ (outliner-property/upsert-property! conn :user.property/num {:type :default :cardinality :many} {})]
       (is (db-property/many? (d/entity @conn :user.property/num)))))
 
   (testing "Multiple properties that generate the same initial :db/ident"
-    (let [conn (create-conn-with-blocks [])]
+    (let [conn (db-test/create-conn-with-blocks [])]
       (outliner-property/upsert-property! conn nil {:type :default} {:property-name "p1"})
       (outliner-property/upsert-property! conn nil {} {:property-name "p1"})
       (outliner-property/upsert-property! conn nil {} {:property-name "p1"})
@@ -65,7 +57,7 @@
 
 (deftest create-property-text-block!
   (testing "Create a new :default property value"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:default "foo"}}
                            {:block/title "b2"}]}])
@@ -82,7 +74,7 @@
           "Has correct created-from-property")))
 
   (testing "Create cases for a new :one :number property value"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:num 2}}
                            {:block/title "b2"}]}])
@@ -105,7 +97,7 @@
           "Wrong value isn't transacted")))
 
   (testing "Create new :many :number property values"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:num-many #{2}}}
                            {:block/title "b2"}]}])
@@ -122,7 +114,7 @@
 
 (deftest set-block-property-basic-cases
   (testing "Set a :number value with existing value"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:num 2}}
                            {:block/title "b2"}]}])
@@ -135,7 +127,7 @@
              (:db/id (:user.property/num (find-block-by-content conn "b2")))))))
 
   (testing "Update a :number value with existing value"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:num 2}}
                            {:block/title "b2" :build/properties {:num 3}}]}])
@@ -149,7 +141,7 @@
 
 (deftest set-block-property-with-non-ref-values
   (testing "Setting :default with same property value reuses existing entity"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:logseq.property/order-list-type "number"}}
                            {:block/title "b2"}]}])
@@ -163,7 +155,7 @@
              (:db/id (:logseq.property/order-list-type (find-block-by-content conn "b2")))))))
 
   (testing "Setting :checkbox with same property value reuses existing entity"
-    (let [conn (create-conn-with-blocks
+    (let [conn (db-test/create-conn-with-blocks
                 [{:page {:block/title "page1"}
                   :blocks [{:block/title "b1" :build/properties {:checkbox true}}
                            {:block/title "b2"}]}])
@@ -176,7 +168,7 @@
       (is (= property-value (:user.property/checkbox (find-block-by-content conn "b2")))))))
 
 (deftest remove-block-property!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}
                 :blocks [{:block/title "b1" :build/properties {:default "foo"}}]}])
         block (find-block-by-content conn "b1")
@@ -188,7 +180,7 @@
     (is (nil? (:user.property/default updated-block)) "Block property is deleted")))
 
 (deftest batch-set-property!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}
                 :blocks [{:block/title "item 1"}
                          {:block/title "item 2"}]}])
@@ -201,7 +193,7 @@
         "Property values are batch set")))
 
 (deftest batch-remove-property!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}
                 :blocks [{:block/title "item 1" :build/properties {:logseq.property/order-list-type "number"}}
                          {:block/title "item 2" :build/properties {:logseq.property/order-list-type "number"}}]}])
@@ -213,7 +205,7 @@
         "Property values are batch removed")))
 
 (deftest add-existing-values-to-closed-values!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}
                 :blocks [{:block/title "b1" :build/properties {:num 1}}
                          {:block/title "b2" :build/properties {:num 2}}]}])
@@ -223,7 +215,7 @@
            (map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
 
 (deftest upsert-closed-value!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               {:properties {:num {:build/closed-values [{:uuid (random-uuid) :value 2}]
                                   :block/schema {:type :number}}}})]
 
@@ -260,7 +252,7 @@
 (deftest delete-closed-value!
   (let [closed-value-uuid (random-uuid)
         used-closed-value-uuid (random-uuid)
-        conn (create-conn-with-blocks
+        conn (db-test/create-conn-with-blocks
               {:properties {:default {:build/closed-values [{:uuid closed-value-uuid :value "foo"}
                                                             {:uuid used-closed-value-uuid :value "bar"}]
                                       :block/schema {:type :default}}}
@@ -273,7 +265,7 @@
     (is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))))
 
 (deftest class-add-property!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               {:classes {:c1 {}}
                :properties {:p1 {:block/schema {:type :default}}
                             :p2 {:block/schema {:type :default}}}})
@@ -283,14 +275,14 @@
            (map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
 
 (deftest class-remove-property!
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               {:classes {:c1 {:build/schema-properties [:p1 :p2]}}})
         _ (outliner-property/class-remove-property! conn :user.class/c1 :user.property/p1)]
     (is (= [:user.property/p2]
            (map :db/ident (:logseq.property.class/properties (d/entity @conn :user.class/c1)))))))
 
 (deftest get-block-classes-properties
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               {:classes {:c1 {:build/schema-properties [:p1]}
                          :c2 {:build/schema-properties [:p2 :p3]}}
                :pages-and-blocks

+ 66 - 14
deps/outliner/test/logseq/outliner/validate_test.cljs

@@ -1,27 +1,19 @@
 (ns logseq.outliner.validate-test
-  (:require [cljs.test :refer [deftest is]]
-            [logseq.db.frontend.schema :as db-schema]
+  (:require [cljs.test :refer [deftest is are testing]]
             [datascript.core :as d]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
-            [logseq.db.sqlite.build :as sqlite-build]
-            [logseq.outliner.validate :as outliner-validate]))
-
-(defn- create-conn-with-blocks [opts]
-  (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
-        _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
-        _ (sqlite-build/create-blocks conn opts)]
-    conn))
+            [logseq.outliner.validate :as outliner-validate]
+            [logseq.db.test.helper :as db-test]))
 
 (defn- find-block-by-content [conn content]
   (->> content
        (d/q '[:find [(pull ?b [*]) ...]
               :in $ ?content
-              :where [?b :block/title ?content]]
+              :where [?b :block/title ?content] [(missing? $ ?b :logseq.property/built-in?)]]
             @conn)
        first))
 
 (deftest validate-block-title-unique-for-properties
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               ;; use a property name that's same as built-in
               {:properties {:background-image {:block/schema {:type :default}}}})]
 
@@ -42,7 +34,7 @@
         "Disallow duplicate user property")))
 
 (deftest validate-block-title-unique-for-pages
-  (let [conn (create-conn-with-blocks
+  (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}}
                {:page {:block/title "Apple" :build/tags [:Company]}}
                {:page {:block/title "Banana" :build/tags [:Fruit]}}])]
@@ -77,3 +69,63 @@
           "Apple"
           (find-block-by-content conn "Fruit")))
         "Allow class to have same name as a page")))
+
+(deftest validate-parent-property
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:prop1 {:block/schema {:type :default}}}
+               :classes {:Class1 {} :Class2 {}}
+               :pages-and-blocks
+               [{:page {:block/title "page1"}}
+                {:page {:block/title "page2"}}]})
+        page1 (find-block-by-content conn "page1")
+        page2 (find-block-by-content conn "page2")
+        class1 (find-block-by-content conn "Class1")
+        class2 (find-block-by-content conn "Class2")
+        property (find-block-by-content conn "prop1")]
+
+    (testing "valid parent and child combinations"
+      (is (nil? (outliner-validate/validate-parent-property page1 [page2]))
+          "parent page to child page is valid")
+      (is (nil? (outliner-validate/validate-parent-property class1 [class2]))
+          "parent class to child class is valid"))
+
+    (testing "invalid parent and child combinations"
+      (are [parent child]
+           (thrown-with-msg?
+            js/Error
+            #"Can't set"
+            (outliner-validate/validate-parent-property parent [child]))
+
+        class1 page1
+        page1 class1
+        property page1
+        property class1))))
+
+;; Try as many of the validations against a new graph to confirm
+;; that validations make sense and are valid for a new graph
+(deftest new-graph-should-be-valid
+  (let [conn (db-test/create-conn)]
+
+    (testing "Validate pages"
+      (let [pages (d/q '[:find [(pull ?b [*]) ...] :where [?b :block/title] [?b :block/type]] @conn)
+            page-errors (atom {})]
+        (doseq [page pages]
+          (try
+            (outliner-validate/validate-unique-by-name-tag-and-block-type @conn (:block/title page) page)
+            (outliner-validate/validate-page-title (:block/title page) {:node page})
+            (outliner-validate/validate-page-title-characters (:block/title page) {:node page})
+
+            (catch :default e
+              (if (= :notification (:type (ex-data e)))
+                (swap! page-errors update (select-keys page [:block/title :db/ident :block/uuid]) (fnil conj []) e)
+                (throw e)))))
+        (is (= {} @page-errors)
+            "Default pages shouldn't have any validation errors")))
+
+    (testing "Validate property relationships"
+      (let [parent-child-pairs (d/q '[:find (pull ?parent [:block/title :block/type])
+                                      (pull ?child [:block/title :block/type])
+                                      :where [?child :logseq.property/parent ?parent]] @conn)]
+        (doseq [[parent child] parent-child-pairs]
+          (is (nil? (outliner-validate/validate-parent-property parent [child]))
+              (str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child)))))))))

+ 14 - 15
deps/publishing/src/logseq/publishing/html.cljs

@@ -23,22 +23,22 @@ necessary db filtering"
 ;; Copied from https://github.com/babashka/babashka/blob/8c1077af00c818ade9e646dfe1297bbe24b17f4d/examples/notes.clj#L21
 (defn- html [v]
   (cond (vector? v)
-    (let [tag (first v)
-          attrs (second v)
-          attrs (when (map? attrs) attrs)
-          elts (if attrs (nnext v) (next v))
-          tag-name (name tag)]
-      (gstring/format "<%s%s>%s</%s>\n" tag-name (html attrs) (html elts) tag-name))
-    (map? v)
-    (string/join ""
-                 (keep (fn [[k v]]
+        (let [tag (first v)
+              attrs (second v)
+              attrs (when (map? attrs) attrs)
+              elts (if attrs (nnext v) (next v))
+              tag-name (name tag)]
+          (gstring/format "<%s%s>%s</%s>\n" tag-name (html attrs) (html elts) tag-name))
+        (map? v)
+        (string/join ""
+                     (keep (fn [[k v]]
                          ;; Skip nil values because some html tags haven't been
                          ;; given values through html-options
-                         (when (some? v)
-                           (gstring/format " %s=\"%s\"" (name k) v))) v))
-    (seq? v)
-    (string/join " " (map html v))
-    :else (str v)))
+                             (when (some? v)
+                               (gstring/format " %s=\"%s\"" (name k) v))) v))
+        (seq? v)
+        (string/join " " (map html v))
+        :else (str v)))
 
 (defn- ^:large-vars/html publishing-html
   [transit-db app-state options]
@@ -54,7 +54,6 @@ necessary db filtering"
              {:content
               "minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no",
               :name "viewport"}]
-            [:link {:type "text/css", :href "static/css/tabler-icons.min.css", :rel "stylesheet"}]
             [:link {:type "text/css", :href "static/css/style.css", :rel "stylesheet"}]
             [:link {:type "text/css", :href "static/css/custom.css", :rel "stylesheet"}]
             [:link {:type "text/css", :href "static/css/export.css", :rel "stylesheet"}]

+ 1 - 1
deps/shui/deps.edn

@@ -2,7 +2,7 @@
  :deps
  {org.clojure/clojure                   {:mvn/version "1.11.1"}
   org.clojure/clojurescript             {:mvn/version "1.11.132"}
-  funcool/promesa                       {:mvn/version "4.0.2"}
+  funcool/promesa                       {:mvn/version "11.0.678"}
   rum/rum                               {:mvn/version "0.12.9"}
   medley/medley                         {:mvn/version "1.4.0"}
   cljs-bean/cljs-bean                   {:mvn/version "1.5.0"}}}

+ 6 - 2
gulpfile.js

@@ -6,6 +6,7 @@ const path = require('path')
 const gulp = require('gulp')
 const del = require('del')
 const ip = require('ip')
+const replace = require('gulp-replace');
 
 const outputPath = path.join(__dirname, 'static')
 const resourcesPath = path.join(__dirname, 'resources')
@@ -69,7 +70,11 @@ const common = {
         'node_modules/react/umd/react.development.js',
         'node_modules/react-dom/umd/react-dom.production.min.js',
         'node_modules/react-dom/umd/react-dom.development.js',
+        'node_modules/prop-types/prop-types.min.js'
       ]).pipe(gulp.dest(path.join(outputPath, 'js'))),
+      () => gulp.src([
+        'node_modules/@tabler/icons-react/dist/umd/tabler-icons-react.min.js'
+      ]).pipe(replace('"@tabler/icons-react"]={},a.react,', '"tablerIcons"]={},a.React,')).pipe(gulp.dest(path.join(outputPath, 'js'))),
       () => gulp.src([
         'node_modules/@glidejs/glide/dist/glide.min.js',
         'node_modules/@glidejs/glide/dist/css/glide.core.min.css',
@@ -84,14 +89,13 @@ const common = {
         'node_modules/pdfjs-dist/cmaps/*.*',
       ]).pipe(gulp.dest(path.join(outputPath, 'js', 'pdfjs', 'cmaps'))),
       () => gulp.src([
-        'node_modules/@tabler/icons/iconfont/tabler-icons.min.css',
         'node_modules/inter-ui/inter.css',
         'node_modules/reveal.js/dist/theme/fonts/source-sans-pro/**',
       ]).pipe(gulp.dest(path.join(outputPath, 'css'))),
       () => gulp.src('node_modules/inter-ui/Inter (web)/*.*')
         .pipe(gulp.dest(path.join(outputPath, 'css', 'Inter (web)'))),
       () => gulp.src([
-        'node_modules/@tabler/icons/iconfont/fonts/**',
+        'node_modules/@tabler/icons-webfont/fonts/**',
         'node_modules/katex/dist/fonts/*.woff2'
       ]).pipe(gulp.dest(path.join(outputPath, 'css', 'fonts'))),
     )(...params)

+ 5 - 2
package.json

@@ -17,6 +17,7 @@
         "del": "^6.0.0",
         "glob": "9.0.0",
         "gulp": "^4.0.2",
+        "gulp-replace": "^1.1.4",
         "gulp-postcss": "^10.0.0",
         "ip": "1.1.9",
         "karma": "^6.4.4",
@@ -116,14 +117,15 @@
         "@radix-ui/colors": "^0.1.8",
         "@sentry/react": "^6.18.2",
         "@sentry/tracing": "^6.18.2",
-        "@tabler/icons": "^1.96.0",
+        "@tabler/icons-react": "^2.47.0",
+        "@tabler/icons-webfont": "^2.47.0",
         "@tippyjs/react": "4.2.5",
         "bignumber.js": "^9.0.2",
         "capacitor-voice-recorder": "^5.0.0",
         "check-password-strength": "2.0.7",
         "chokidar": "3.5.1",
         "chrono-node": "2.2.4",
-        "codemirror": "5.65.13",
+        "codemirror": "5.65.18",
         "comlink": "^4.4.1",
         "d3-force": "3.0.0",
         "diff": "5.0.0",
@@ -151,6 +153,7 @@
         "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",
         "posthog-js": "1.10.2",
+        "prop-types": "^15.7.2",
         "react": "17.0.2",
         "react-dom": "17.0.2",
         "react-grid-layout": "0.16.6",

+ 0 - 1
public/index.html

@@ -7,7 +7,6 @@
     window.indexedDB;
   </script>
   <meta content="minimum-scale=1, initial-scale=1, maximum-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
-  <link rel="stylesheet" href="/static/css/tabler-icons.min.css">
   <link href="/static/css/style.css" rel="stylesheet" type="text/css">
   <link href="/static/img/logo.png" rel="shortcut icon" type="image/png">
   <link href="/static/img/logo.png" rel="shortcut icon" sizes="192x192">

+ 1 - 1
resources/css/codemirror.lsradix.css

@@ -36,7 +36,7 @@ http://ethanschoonover.com/lsradix/img/lsradix-palette.png
 }
 .cm-s-lsradix.cm-s-dark {
   background-color: var(--lx-gray-01, hsl(var(--secondary)/.7));
-  color: var(--lx-gray-10, hsl(var(--secondary-foreground)));
+  color: var(--ls-primary-text-color, var(--lx-gray-10, hsl(var(--secondary-foreground))));
 }
 
 .dark .cm-s-lsradix.cm-s-dark {

+ 0 - 1
resources/index.html

@@ -3,7 +3,6 @@
 <head>
   <meta charset="utf-8">
   <meta content="minimum-scale=1, initial-scale=1, maximum-scale=1, width=device-width, shrink-to-fit=no" name="viewport">
-  <link rel="stylesheet" type="text/css" href="./css/tabler-icons.min.css">
   <link href="./css/style.css" rel="stylesheet" type="text/css">
   <link href="./img/logo.png" rel="shortcut icon" type="image/png">
   <link href="./img/logo.png" rel="shortcut icon" sizes="192x192">

+ 0 - 1
resources/js/prop-types.min.js

@@ -1 +0,0 @@
-!function(f){"object"==typeof exports&&"undefined"!=typeof module?module.exports=f():"function"==typeof define&&define.amd?define([],f):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).PropTypes=f()}(function(){return function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var p="function"==typeof require&&require;if(!f&&p)return p(i,!0);if(u)return u(i,!0);throw(p=new Error("Cannot find module '"+i+"'")).code="MODULE_NOT_FOUND",p}p=n[i]={exports:{}},e[i][0].call(p.exports,function(r){return o(e[i][1][r]||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}({1:[function(require,module,exports){"use strict";var ReactPropTypesSecret=require(3);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,module.exports=function(){function e(e,t,n,r,o,c){if(c!==ReactPropTypesSecret){c=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw c.name="Invariant Violation",c}}function t(){return e}var n={array:e.isRequired=e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return n.PropTypes=n}},{3:3}],2:[function(require,module,exports){module.exports=require(1)()},{1:1}],3:[function(require,module,exports){"use strict";module.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},{}]},{},[2])(2)});

File diff suppressed because it is too large
+ 0 - 4
resources/js/tabler-icons-react.min.js


File diff suppressed because it is too large
+ 0 - 0
resources/js/tabler.min.js


+ 35 - 42
scripts/src/logseq/tasks/db_graph/create_graph_with_properties.cljs

@@ -25,7 +25,7 @@
   (new js/Date (- (.getTime date) (* days 24 60 60 1000))))
 
 (defn- build-closed-values-config
-  [_opts]
+  []
   {:default-closed
    (mapv #(hash-map :value %
                     :uuid (random-uuid)
@@ -38,10 +38,7 @@
    :number-closed
    (mapv #(hash-map :value %
                     :uuid (random-uuid))
-         [10 42 (rand 100)])
-   ;; If this is enabled again, :uuid translation support would need to be added for :build/closed-values
-   :date-closed
-   {}})
+         [10 42 (rand 100)])})
 
 (defn- query [query-string]
   {:block/title query-string
@@ -53,7 +50,7 @@
   (let [today (new js/Date)
         yesterday (subtract-days today 1)
         two-days-ago (subtract-days today 2)
-        closed-values-config (build-closed-values-config {:dates [today yesterday two-days-ago]})
+        closed-values-config (build-closed-values-config)
         ;; Stores random closed values for use with queries
         closed-values (atom {})
         random-closed-value #(let [val (-> closed-values-config % rand-nth)]
@@ -105,27 +102,24 @@
           {:block/title "date property block" :build/properties {:date [:page (date-journal-title today)]}}
           {:block/title "date-many property block" :build/properties {:date-many #{[:page (date-journal-title today)]
                                                                                    [:page (date-journal-title yesterday)]}}}
-          {:block/title "datetime property block" :build/properties {:datetime (common-util/time-ms)}}
-          ;; :date-closed disabled for now since they're not supported
-          #_{:block/title "date-closed property block" :build/properties {:date-closed (random-closed-value :date-closed)}}]}
+          {:block/title "datetime property block" :build/properties {:datetime (common-util/time-ms)}}]}
         {:page {:block/title "Block Property Queries"}
          :blocks
-         [(query "(property :default \"haha\")")
-          (query "(property :default-many \"haw\")")
-          (query (str "(property :default-closed " (pr-str (get-closed-value :default-closed)) ")"))
-          (query "(property :url \"https://logseq.com\")")
-          (query "(property :url-many \"https://logseq.com\")")
-          (query (str "(property :url-closed " (pr-str (get-closed-value :url-closed)) ")"))
-          (query "(property :checkbox true)")
-          (query "(property :number 5)")
-          (query "(property :number-many 10)")
-          (query (str "(property :number-closed " (pr-str (get-closed-value :number-closed)) ")"))
-          (query "(property :node \"block object\")")
-          (query "(property :node-without-classes [[Page 1]])")
-          (query "(property :node-many [[Page object]])")
-          (query (str "(property :date " (page-ref/->page-ref (string/capitalize (date-journal-title today))) ")"))
-          (query (str "(property :date-many " (page-ref/->page-ref (string/capitalize (date-journal-title yesterday))) ")"))
-          #_(query (str "(property :date-closed " (page-ref/->page-ref (string/capitalize (get-closed-value :date-closed))) ")"))]}
+         [(query "(property default \"haha\")")
+          (query "(property default-many \"haw\")")
+          (query (str "(property default-closed " (pr-str (get-closed-value :default-closed)) ")"))
+          (query "(property url \"https://logseq.com\")")
+          (query "(property url-many \"https://logseq.com\")")
+          (query (str "(property url-closed " (pr-str (get-closed-value :url-closed)) ")"))
+          (query "(property checkbox true)")
+          (query "(property number 5)")
+          (query "(property number-many 10)")
+          (query (str "(property number-closed " (pr-str (get-closed-value :number-closed)) ")"))
+          (query "(property node \"block object\")")
+          (query "(property node-without-classes [[Page 1]])")
+          (query "(property node-many [[Page object]])")
+          (query (str "(property date " (page-ref/->page-ref (string/capitalize (date-journal-title today))) ")"))
+          (query (str "(property date-many " (page-ref/->page-ref (string/capitalize (date-journal-title yesterday))) ")"))]}
 
         ;; Page property pages and queries
         {:page {:block/title "default page" :build/properties {:default "yolo"}}}
@@ -145,25 +139,24 @@
         {:page {:block/title "date-many page" :build/properties {:date-many #{[:page (date-journal-title today)]
                                                                               [:page (date-journal-title yesterday)]}}}}
         {:page {:block/title "datetime page" :build/properties {:datetime (common-util/time-ms)}}}
-        #_{:page {:block/title "date-closed page" :build/properties {:date-closed (random-closed-value :date-closed)}}}
+
         {:page {:block/title "Page Property Queries"}
          :blocks
-         [(query "(page-property :default \"yolo\")")
-          (query "(page-property :default-many \"haw\")")
-          (query (str "(page-property :default-closed " (pr-str (get-closed-value :default-closed)) ")"))
-          (query "(page-property :url \"https://logseq.com\")")
-          (query "(page-property :url-many \"https://logseq.com\")")
-          (query (str "(page-property :url-closed " (pr-str (get-closed-value :url-closed)) ")"))
-          (query "(page-property :checkbox true)")
-          (query "(page-property :number 5)")
-          (query "(page-property :number-many 10)")
-          (query (str "(page-property :number-closed " (pr-str (get-closed-value :number-closed)) ")"))
-          (query "(page-property :node \"block object\")")
-          (query "(page-property :node-without-classes [[Page 1]])")
-          (query "(page-property :node-many [[Page object]])")
-          (query (str "(page-property :date " (page-ref/->page-ref (string/capitalize (date-journal-title today))) ")"))
-          (query (str "(page-property :date-many " (page-ref/->page-ref (string/capitalize (date-journal-title yesterday))) ")"))
-          #_(query (str "(page-property :date-closed " (page-ref/->page-ref (string/capitalize (get-closed-value :date-closed))) ")"))]}]))
+         [(query "(page-property default \"yolo\")")
+          (query "(page-property default-many \"haw\")")
+          (query (str "(page-property default-closed " (pr-str (get-closed-value :default-closed)) ")"))
+          (query "(page-property url \"https://logseq.com\")")
+          (query "(page-property url-many \"https://logseq.com\")")
+          (query (str "(page-property url-closed " (pr-str (get-closed-value :url-closed)) ")"))
+          (query "(page-property checkbox true)")
+          (query "(page-property number 5)")
+          (query "(page-property number-many 10)")
+          (query (str "(page-property number-closed " (pr-str (get-closed-value :number-closed)) ")"))
+          (query "(page-property node \"block object\")")
+          (query "(page-property node-without-classes [[Page 1]])")
+          (query "(page-property node-many [[Page object]])")
+          (query (str "(page-property date " (page-ref/->page-ref (string/capitalize (date-journal-title today))) ")"))
+          (query (str "(page-property date-many " (page-ref/->page-ref (string/capitalize (date-journal-title yesterday))) ")"))]}]))
 
      :classes {:TestClass {}}
 

+ 2 - 0
scripts/src/logseq/tasks/dev/db_and_file_graphs.clj

@@ -98,6 +98,8 @@
                               "block/name"
                               ;; anything org mode
                               "org"
+                              "#+BEGIN_"
+                              "#+END_"
                               "pre-block"
                               "db/get-page"
                               "/page-name-sanity-lc"]))

+ 78 - 44
src/main/frontend/commands.cljs

@@ -141,7 +141,7 @@
 (defn db-based-query
   []
   [[:editor/input "" {:last-pattern command-trigger}]
-   [:editor/set-property :block/tags :logseq.class/Query]])
+   [:editor/run-query-command]])
 
 (defn file-based-query
   []
@@ -154,11 +154,58 @@
     (db-based-query)
     (file-based-query)))
 
+(defn- calc-steps
+  []
+  (if (config/db-based-graph? (state/get-current-repo))
+    [[:editor/input "" {:last-pattern command-trigger}]
+     [:editor/upsert-type-block :code "calc"]
+     [:codemirror/focus]]
+    [[:editor/input "```calc\n\n```" {:type "block"
+                                      :backward-pos 4}]
+     [:codemirror/focus]]))
+
+(defn ->block
+  ([type]
+   (->block type nil))
+  ([type optional]
+   (let [format (get (state/get-edit-block) :block/format)
+         markdown-src? (and (= format :markdown)
+                            (= (string/lower-case type) "src"))
+         [left right] (cond
+                        markdown-src?
+                        ["```" "\n```"]
+
+                        :else
+                        (->> ["#+BEGIN_%s" "\n#+END_%s"]
+                             (map #(util/format %
+                                                (string/upper-case type)))))
+         template (str
+                   left
+                   (if optional (str " " optional) "")
+                   "\n"
+                   right)
+         backward-pos (if (= type "src")
+                        (+ 1 (count right))
+                        (count right))]
+     [[:editor/input template {:type "block"
+                               :last-pattern command-trigger
+                               :backward-pos backward-pos}]])))
+
+(defn- advanced-query-steps
+  []
+  (if (config/db-based-graph? (state/get-current-repo))
+    [[:editor/input "" {:last-pattern command-trigger}]
+     [:editor/set-property :block/tags :logseq.class/Query]
+     [:editor/set-property :logseq.property/query ""]
+     [:editor/set-property-on-block-property :logseq.property/query :logseq.property.node/display-type :code]
+     [:editor/set-property-on-block-property :logseq.property/query :logseq.property.code/lang "clojure"]]
+    (->block "query")))
+
 (defn db-based-code-block
   []
   [[:editor/input "" {:last-pattern command-trigger}]
-   [:editor/set-property :logseq.property.node/type :code]
-   [:codemirror/focus]])
+   [:editor/upsert-type-block :code]
+   [:editor/exit]])
 
 (defn file-based-code-block
   []
@@ -173,19 +220,18 @@
     (db-based-code-block)
     (file-based-code-block)))
 
-(declare ->block)
 (defn quote-block-steps
   []
   (if (config/db-based-graph? (state/get-current-repo))
     [[:editor/input "" {:last-pattern command-trigger}]
-     [:editor/set-property :logseq.property.node/type :quote]]
+     [:editor/set-property :logseq.property.node/display-type :quote]]
     (->block "quote")))
 
 (defn math-block-steps
   []
   (if (config/db-based-graph? (state/get-current-repo))
     [[:editor/input "" {:last-pattern command-trigger}]
-     [:editor/set-property :logseq.property.node/type :math]]
+     [:editor/set-property :logseq.property.node/display-type :math]]
     (->block "export" "latex")))
 
 (defn get-statuses
@@ -256,33 +302,6 @@
 (defonce *matched-commands (atom nil))
 (defonce *initial-commands (atom nil))
 
-(defn ->block
-  ([type]
-   (->block type nil))
-  ([type optional]
-   (let [format (get (state/get-edit-block) :block/format)
-         markdown-src? (and (= format :markdown)
-                            (= (string/lower-case type) "src"))
-         [left right] (cond
-                        markdown-src?
-                        ["```" "\n```"]
-
-                        :else
-                        (->> ["#+BEGIN_%s" "\n#+END_%s"]
-                             (map #(util/format %
-                                                (string/upper-case type)))))
-         template (str
-                   left
-                   (if optional (str " " optional) "")
-                   "\n"
-                   right)
-         backward-pos (if (= type "src")
-                        (+ 1 (count right))
-                        (count right))]
-     [[:editor/input template {:type "block"
-                               :last-pattern command-trigger
-                               :backward-pos backward-pos}]])))
-
 (defn ->properties
   []
   [[:editor/clear-current-slash]
@@ -375,7 +394,6 @@
         (cond->
          [;; Should this be replaced by "Code block"?
           ["Src" (->block "src") "Create a code block"]
-          ["Advanced Query" (->block "query") "Create an advanced query block"]
           ["Math block" (->block "export" "latex") "Create a latex block"]
           ["Note" (->block "note") "Create a note block"]
           ["Tip" (->block "tip") "Create a tip block"]
@@ -394,17 +412,14 @@
           (conj ["Properties" (->properties)])))
 
       ;; advanced
-      [["Query"
-        (query-steps)
-        query-doc
-        :icon/query
-        "ADVANCED"]
+      [["Query" (query-steps) query-doc :icon/query "ADVANCED"]
+       ["Advanced Query" (advanced-query-steps) "Create an advanced query block" :icon/query]
        (when-not db?
          ["Zotero" (zotero-steps) "Import Zotero journal article" :icon/circle-letter-z])
        ["Query function" [[:editor/input "{{function }}" {:backward-pos 2}]] "Create a query function" :icon/queryCode]
-       ["Calculator" [[:editor/input "```calc\n\n```" {:type "block"
-                                                       :backward-pos 4}]
-                      [:codemirror/focus]] "Insert a calculator" :icon/calculator]
+       ["Calculator"
+        (calc-steps)
+        "Insert a calculator" :icon/calculator]
        (when-not db?
          ["Draw" (fn []
                    (let [file (draw/file-name)
@@ -739,6 +754,19 @@
     (when-let [block (state/get-edit-block)]
       (db-property-handler/set-block-property! (:db/id block) property-id value))))
 
+(defmethod handle-step :editor/set-property-on-block-property [[_ block-property-id property-id value]]
+  (when (config/db-based-graph? (state/get-current-repo))
+    (let [updated-block (when-let [block-uuid (:block/uuid (state/get-edit-block))]
+                          (db/entity [:block/uuid block-uuid]))
+          block-property-value (get updated-block block-property-id)]
+      (when block-property-value
+        (db-property-handler/set-block-property! (:db/id block-property-value) property-id value)))))
+
+(defmethod handle-step :editor/upsert-type-block [[_ type lang]]
+  (when (config/db-based-graph? (state/get-current-repo))
+    (when-let [block (state/get-edit-block)]
+      (state/pub-event! [:editor/upsert-type-block {:block block :type type :lang lang}]))))
+
 (defn- file-based-set-priority
   [priority]
   (when-let [input-id (state/get-edit-input-id)]
@@ -771,6 +799,9 @@
     (state/pub-event! [:editor/new-property {:property-key "Deadline"}])
     (handle-step [:editor/show-date-picker :deadline])))
 
+(defmethod handle-step :editor/run-query-command [[_]]
+  (state/pub-event! [:editor/run-query-command]))
+
 (defmethod handle-step :editor/insert-properties [[_ _] _format]
   (when-let [input-id (state/get-edit-input-id)]
     (when-let [current-input (gdom/getElement input-id)]
@@ -897,8 +928,11 @@
 
 (defn handle-steps
   [vector' format]
-  (doseq [step vector']
-    (handle-step step format)))
+  (if (config/db-based-graph? (state/get-current-repo))
+    (p/doseq [step vector']
+      (handle-step step format))
+    (doseq [step vector']
+      (handle-step step format))))
 
 (defn exec-plugin-simple-command!
   [pid {:keys [block-id] :as cmd} action]

+ 208 - 126
src/main/frontend/components/block.cljs

@@ -9,9 +9,7 @@
             [datascript.impl.entity :as e]
             [dommy.core :as dom]
             [electron.ipc :as ipc]
-            [frontend.commands :as commands]
             [frontend.components.block.macros :as block-macros]
-            [frontend.components.datetime :as datetime-comp]
             [frontend.components.file-based.block :as file-block]
             [frontend.components.icon :as icon-component]
             [frontend.components.lazy-editor :as lazy-editor]
@@ -23,6 +21,7 @@
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.svg :as svg]
             [frontend.components.title :as title]
+            [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
@@ -53,7 +52,6 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.property.file :as property-file]
             [frontend.handler.property.util :as pu]
-            [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
@@ -91,7 +89,8 @@
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
-            [shadow.loader :as loader]))
+            [shadow.loader :as loader]
+            [frontend.storage :as storage]))
 
 ;; local state
 (defonce *dragging?
@@ -323,7 +322,7 @@
                   :on-click
                   (fn [e]
                     (util/stop e)
-                    (when-let [block-id (:block/uuid config)]
+                    (when-let [block-id (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
                       (let [*local-selected? (atom local?)]
                         (-> (shui/dialog-confirm!
                              [:div.text-xs.opacity-60.-my-2
@@ -1430,9 +1429,10 @@
   [config arguments]
   [:div.dsl-query.pr-3.sm:pr-0
    (let [query (->> (string/join ", " arguments)
-                    (string/trim))]
+                    (string/trim))
+         build-option (assoc (:block config) :file-version/query-macro-title query)]
      (query/custom-query (wrap-query-components (assoc config :dsl-query? true))
-                         {:builder (query-builder-component/builder query config)
+                         {:builder (query-builder-component/builder build-option {})
                           :query query}))])
 
 (defn- macro-function-cp
@@ -1656,7 +1656,7 @@
     (cond
       (= name "query")
       (if (config/db-based-graph? (state/get-current-repo))
-        [:div.warning "{{query}} has been deprecated. Use `/Query` command instead."]
+        [:div.warning "{{query}} is deprecated. Use '/Query' command instead."]
         (macro-query-cp config arguments))
 
       (= name "function")
@@ -1664,7 +1664,7 @@
 
       (= name "namespace")
       (if (config/db-based-graph? (state/get-current-repo))
-        [:div.warning "Namespace has been deprecated, use tags instead"]
+        [:div.warning "Namespace is deprecated, use tags instead"]
         (let [namespace (first arguments)]
           (when-not (string/blank? namespace)
             (let [namespace (string/lower-case (page-ref/get-page-name! namespace))
@@ -1715,7 +1715,7 @@
 
       (= name "embed")
       (if (config/db-based-graph? (state/get-current-repo))
-        [:div.warning "embed has been deprecated. Use / command 'Node embed' instead."]
+        [:div.warning "{{embed}} is deprecated. Use '/Node embed' command instead."]
         (macro-embed-cp config arguments))
 
       (= name "renderer")
@@ -2054,10 +2054,11 @@
 (declare block-content)
 
 (declare src-cp)
+(declare block-title)
 
-(defn- text-block-title
+(rum/defc ^:large-vars/cleanup-todo text-block-title
   [config {:block/keys [marker pre-block? properties] :as block}]
-  (let [block-title (:block.temp/ast-title block)
+  (let [block-ast-title (:block.temp/ast-title block)
         config (assoc config :block block)
         level (:level config)
         slide? (boolean (:slide? config))
@@ -2138,33 +2139,57 @@
          (when-not area? [(hl-ref)])
 
          (conj
-          (map-inline config block-title)
+          (map-inline config block-ast-title)
           (when (= block-type :whiteboard-shape) [:span.mr-1 (ui/icon "whiteboard-element" {:extension? true})]))
 
          ;; highlight ref block (area)
          (when area? [(hl-ref)])
 
-         (when (and (seq block-title) (ldb/class-instance? (db/entity :logseq.class/Cards) block))
-           [(shui/button
-             {:variant :ghost
-              :size :sm
-              :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
-              :on-click (fn [_]
-                          (state/pub-event! [:modal/show-cards (:db/id block)]))}
-             "Practice")])))))))
+         (when (and (seq block-ast-title) (ldb/class-instance? (db/entity :logseq.class/Cards) block))
+           [(ui/tooltip
+             (shui/button
+              {:variant :ghost
+               :size :sm
+               :class "ml-2 !px-1 !h-5 text-xs text-muted-foreground"
+               :on-click (fn [e]
+                           (util/stop e)
+                           (state/pub-event! [:modal/show-cards (:db/id block)]))}
+              "Practice")
+             [:div "Practice cards"])])))))))
 
-(defn build-block-title
+(rum/defc block-title < rum/reactive db-mixins/query
   [config block]
-  (let [node-type (:logseq.property.node/type block)]
-    (case node-type
-      :code
+  (let [collapsed? (:collapsed? config)
+        block' (db/entity (:db/id block))
+        node-type (:logseq.property.node/display-type block')
+        query? (ldb/class-instance? (db/entity :logseq.class/Query) block')
+        query (:logseq.property/query block')
+        advanced-query? (and query? (= :code node-type))]
+    (cond
+      (= :code node-type)
       [:div.flex.flex-1.w-full
-       (src-cp (assoc config :block block) {:language (:logseq.property.code/mode block)})]
+       (src-cp (assoc config :block block) {:language (:logseq.property.code/lang block)})]
 
       ;; TODO: switched to https://cortexjs.io/mathlive/ for editing
-      :math
+      (= :math node-type)
       (latex/latex (str (:container-id config) "-" (:db/id block)) (:block/title block) true false)
 
+      (and query?
+           collapsed?
+           (not advanced-query?)
+           (string/blank? (:block/title block'))
+           (seq (:block/title query)))
+      (text-block-title config
+                        (merge query
+                               (block/parse-title-and-body (:block/uuid query) :markdown false (:block/title query))))
+
+      (seq (:logseq.property/_query block'))
+      (query-builder-component/builder block' {})
+
+      (and query? (string/blank? (:block/title block')))
+      [:span.opacity-50 "Set query title"]
+
+      :else
       (text-block-title config block))))
 
 (rum/defc span-comma
@@ -2270,48 +2295,6 @@
         [:button.p-1.mr-2 p])]
      [:code "Property name begins with a non-numeric character and can contain alphanumeric characters and . * + ! - _ ? $ % & = < >. If -, + or . are the first character, the second character (if any) must be non-numeric."]]))
 
-(rum/defc timestamp-editor
-  [ast *show-datapicker?]
-
-  (let [*trigger-ref (rum/use-ref nil)]
-    (rum/use-effect!
-     (fn []
-       (let [pid (shui/popup-show!
-                  (.closest (rum/deref *trigger-ref) "a")
-                  (datetime-comp/date-picker nil nil (repeated/timestamp->map ast))
-                  {:id :timestamp-editor
-                   :align :start
-                   :root-props {:onOpenChange #(reset! *show-datapicker? %)}
-                   :content-props {:onEscapeKeyDown #(reset! *show-datapicker? false)}})]
-         #(do (shui/popup-hide! pid)
-              (reset! *show-datapicker? false))))
-     [])
-    [:i {:ref *trigger-ref}]))
-
-(rum/defcs timestamp-cp
-  < rum/reactive
-  (rum/local false ::show-datepicker?)
-  [state block typ ast]
-  (let [ts-block-id (get-in (state/sub [:editor/set-timestamp-block]) [:block :block/uuid])
-        _active? (= (get block :block/uuid) ts-block-id)
-        *show-datapicker? (get state ::show-datepicker?)]
-    [:div.flex.flex-col.gap-4.timestamp
-     [:div.text-sm.flex.flex-row
-      [:div.opacity-50.font-medium.timestamp-label
-       (str typ ": ")]
-      [:a.opacity-80.hover:opacity-100
-       {:on-pointer-down (fn [e]
-                           (util/stop e)
-                           (state/clear-editor-action!)
-                           (editor-handler/escape-editing)
-                           (reset! *show-datapicker? true)
-                           (reset! commands/*current-command typ)
-                           (state/set-timestamp-block! {:block block
-                                                        :typ typ}))}
-       [:span.time-start "<"] [:time (repeated/timestamp->text ast)] [:span.time-stop ">"]
-       (when (and _active? @*show-datapicker?)
-         (timestamp-editor ast *show-datapicker?))]]]))
-
 (defn- target-forbidden-edit?
   [target]
   (or
@@ -2392,7 +2375,8 @@
                           content
                           block
                           cursor-range
-                          {:move-cursor? false
+                          {:db (db/get-db)
+                           :move-cursor? false
                            :container-id (:container-id config)}))]
                 ;; wait a while for the value of the caret range
                 (p/do!
@@ -2428,7 +2412,7 @@
       :block-content-slotted
       (-> block (dissoc :block/children :block/page)))]
 
-    (when-not (contains? #{:code :math} (:logseq.property.node/type block))
+    (when-not (contains? #{:code :math} (:logseq.property.node/display-type block))
       (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
         (when (and (not block-ref-with-title?)
                    (seq body)
@@ -2546,12 +2530,12 @@
     (when (seq properties)
       (case position
         :block-below
-        [:div.positioned-properties.block-below.flex.flex-row.gap-1.item-center.flex-wrap.text-sm.overflow-x-hidden.max-h-6
+        [:div.positioned-properties.block-below.flex.flex-row.gap-2.item-center.flex-wrap.text-sm.overflow-x-hidden
          (for [pid properties]
            (let [property (db/entity pid)
                  v (get block pid)]
              [:div.flex.flex-row.items-center.gap-2.hover:bg-secondary.rounded
-              [:div.flex.flex-row.opacity-50.hover:opacity-100
+              [:div.flex.flex-row.opacity-50.hover:opacity-100.items-center
                (property-component/property-key-cp block property opts)
                [:div.select-none ":"]]
               (pv/property-value block property v opts)]))]
@@ -2563,14 +2547,14 @@
 
 (rum/defc ^:large-vars/cleanup-todo block-content < rum/reactive
   [config {:block/keys [uuid properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
-  (let [repo (state/get-current-repo)
+  (let [collapsed? (:collapsed? config)
+        repo (state/get-current-repo)
         content (if (config/db-based-graph? (state/get-current-repo))
                   (:block/raw-title block)
                   (property-util/remove-built-in-properties format (:block/raw-title block)))
         block (merge block (block/parse-title-and-body uuid format pre-block? content))
         ast-body (:block.temp/ast-body block)
         ast-title (:block.temp/ast-title block)
-        collapsed? (util/collapsed? block)
         block (assoc block :block/title content)
         plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))
         block-ref? (:block-ref? config)
@@ -2621,6 +2605,7 @@
                                             (block-content-on-pointer-down e block block-id content edit-input-id config))))))]
     [:div.block-content.inline
      (cond-> {:id (str "block-content-" uuid)
+              :key (str "block-content-" uuid)
               :on-pointer-up (fn [e]
                                (when (and
                                       (state/in-selection-mode?)
@@ -2640,17 +2625,17 @@
       [:div.flex.flex-row.justify-between.block-content-inner
        (when-not plugin-slotted?
          [:div.block-head-wrap
-          (build-block-title config block)])
+          (block-title config block)])
 
        (file-block/clock-summary-cp block ast-body)]
 
       (when deadline
         (when-let [deadline-ast (block-handler/get-deadline-ast block)]
-          (timestamp-cp block "DEADLINE" deadline-ast)))
+          (file-block/timestamp-cp block "DEADLINE" deadline-ast)))
 
       (when scheduled
         (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
-          (timestamp-cp block "SCHEDULED" scheduled-ast)))
+          (file-block/timestamp-cp block "SCHEDULED" scheduled-ast)))
 
       (when-not (config/db-based-graph? repo)
         (when-let [invalid-properties (:block/invalid-properties block)]
@@ -2739,14 +2724,17 @@
         refs-count (if (seq (:block/_refs block))
                      (count (:block/_refs block))
                      (rum/react *refs-count))
-        table? (:table? config)]
+        table? (:table? config)
+        type-block-editor? (and (contains? #{:code} (:logseq.property.node/display-type block))
+                                (not= (:db/id block) (:db/id (state/sub :editor/raw-mode-block))))
+        config (assoc config :block-parent-id block-id)]
     [:div.block-content-or-editor-wrap
      {:class (when (:page-title? config) "ls-page-title-container")
-      :data-node-type (some-> (:logseq.property.node/type block) name)}
+      :data-node-type (some-> (:logseq.property.node/display-type block) name)}
      (when (and db-based? (not table?)) (block-positioned-properties config block :block-left))
      [:div.block-content-or-editor-inner
       [:div.flex.flex-1.flex-row.gap-1.items-center
-       (if (and edit? editor-box)
+       (if (and edit? editor-box (not type-block-editor?))
          [:div.editor-wrapper.flex.flex-1
           {:id editor-id
            :class (util/classnames [{:opacity-50 (boolean (or (ldb/built-in? block) (ldb/journal? block)))}])}
@@ -2767,7 +2755,8 @@
                                                           (:block/title block))]
                                           (editor-handler/clear-selection!)
                                           (editor-handler/unhighlight-blocks!)
-                                          (state/set-editing! edit-input-id content block "" {:container-id (:container-id config)}))}})
+                                          (state/set-editing! edit-input-id content block "" {:db (db/get-db)
+                                                                                              :container-id (:container-id config)}))}})
            (block-content config block edit-input-id block-id slide?))
 
           (when (and db-based? (not table?)) (block-positioned-properties config block :block-right))
@@ -3127,6 +3116,24 @@
       [block* result]
       [nil result])))
 
+(rum/defc query-property-cp < rum/reactive db-mixins/query
+  [block config collapsed?]
+  (let [block (db/entity (:db/id block))
+        query? (ldb/class-instance? (db/entity :logseq.class/Query) block)]
+    (when (and query? (not collapsed?))
+      (let [query-id (:db/id (:logseq.property/query block))
+            query (some-> query-id db/sub-block)
+            advanced-query? (= :code (:logseq.property.node/display-type query))]
+        (cond
+          (and advanced-query? (not collapsed?))
+          [:div.flex.flex-1.my-1 {:style {:margin-left 42}}
+           (src-cp (assoc config :block query)
+                   {:language "clojure"})]
+
+          (and (not advanced-query?) (not collapsed?))
+          [:div.my-1 {:style {:margin-left 42}}
+           (block-container (assoc config :property? true) query)])))))
+
 (rum/defcs ^:large-vars/cleanup-todo block-container-inner < rum/reactive db-mixins/query
   {:init (fn [state]
            (let [*ref (atom nil)
@@ -3167,6 +3174,7 @@
 
                      :else
                      db-collapsed?)
+        config (assoc config :collapsed? collapsed?)
         breadcrumb-show? (:breadcrumb-show? config)
         *show-left-menu? (::show-block-left-menu? container-state)
         *show-right-menu? (::show-block-right-menu? container-state)
@@ -3299,7 +3307,8 @@
                    hide-block-refs-count? (or (and (:embed? config)
                                                    (= (:block/uuid block) (:embed-id config)))
                                               table?)]
-               (block-content-or-editor config block
+               (block-content-or-editor config
+                                        block
                                         {:edit-input-id edit-input-id
                                          :block-id block-id
                                          :edit? editing?
@@ -3311,14 +3320,20 @@
         (when (and @*show-right-menu? (not in-whiteboard?) (not (or table? property?)))
           (block-right-menu config block editing?))])
 
+     (when-not (:table? config)
+       (query-property-cp block config collapsed?))
+
      (when (and db-based? (not collapsed?) (not (or table? property?)))
        [:div (when-not (:page-title? config) {:style {:padding-left 45}})
         (db-properties-cp config block {:in-block-container? true})])
 
-     (when (and db-based? (not collapsed?) (not (or table? property?)) (not (string/blank? (:block/title (:logseq.property/query block)))))
-       (let [query (:block/title (:logseq.property/query block))
+     (when (and db-based? (not collapsed?) (not (or table? property?))
+                (ldb/class-instance? (db/entity :logseq.class/Query) block))
+       (let [query-block (:logseq.property/query (db/entity (:db/id block)))
+             query-block (if query-block (db/sub-block (:db/id query-block)) query-block)
+             query (:block/title query-block)
              result (common-util/safe-read-string query)
-             advanced-query? (and (map? result) (:query result))]
+             advanced-query? (map? result)]
          [:div {:style {:padding-left 42}}
           (query/custom-query (wrap-query-components (assoc config
                                                             :dsl-query? (not advanced-query?)
@@ -3543,39 +3558,100 @@
 
 (declare ->hiccup)
 
+(defn- get-code-mode-by-lang
+  [lang]
+  (some (fn [m] (when (= (.-name m) lang) (.-mode m))) js/window.CodeMirror.modeInfo))
+
+(rum/defc src-lang-picker
+  [block on-select!]
+  (when-let [langs (map (fn [m] (.-name m)) js/window.CodeMirror.modeInfo)]
+    (let [options (map (fn [lang] {:label lang :value lang}) langs)]
+      (select/select {:items options
+                      :input-default-placeholder "Choose language"
+                      :on-chosen
+                      (fn [chosen _ _ e]
+                        (let [lang (:value chosen)]
+                          (when (and (= :code (:logseq.property.node/display-type block))
+                                     (not= lang (:logseq.property.code/lang block)))
+                            (on-select! lang e)))
+                        (shui/popup-hide!))}))))
+
 (rum/defc src-cp < rum/static
   [config options]
-  (when options
-    (let [html-export? (:html-export? config)
-          {:keys [lines language]} options
-          attr (when language
-                 {:data-lang language})
-          code (if lines (apply str lines) (:block/title (:block config)))
-          [inside-portal? set-inside-portal?] (rum/use-state nil)]
-      (cond
-        html-export?
-        (highlight/html-export attr code)
+  (let [block (:block config)
+        container-id (:container-id config)
+        *mode-ref (rum/use-ref nil)
+        *actions-ref (rum/use-ref nil)]
+
+    (when options
+      (let [html-export? (:html-export? config)
+            {:keys [lines language]} options
+            attr (when language
+                   {:data-lang language})
+            code (if lines (apply str lines) (:block/title block))
+            [inside-portal? set-inside-portal?] (rum/use-state nil)]
+        (cond
+          html-export?
+          (highlight/html-export attr code)
 
-        :else
-        (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
-          [:div.ui-fenced-code-editor.flex.flex-1
-           {:ref (fn [el]
-                   (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))}
-           (cond
-             (nil? inside-portal?) nil
-
-             (or (:slide? config) inside-portal?)
-             (highlight/highlight (str (random-uuid))
-                                  {:class     (str "language-" language)
-                                   :data-lang language}
-                                  code)
-
-             :else
-             [:<>
-              (lazy-editor/editor config (str (d/squuid)) attr code options)
-              (let [options (:options options) block (:block config)]
-                (when (and (= language "clojure") (contains? (set options) ":results"))
-                  (sci/eval-result code block)))])])))))
+          :else
+          (let [language (if (contains? #{"edn" "clj" "cljc" "cljs"} language) "clojure" language)]
+            [:div.ui-fenced-code-editor.flex.flex-1
+             {:ref (fn [el]
+                     (set-inside-portal? (and el (whiteboard-handler/inside-portal? el))))
+              :on-mouse-over #(dom/add-class! (rum/deref *actions-ref) "opacity-100")
+              :on-mouse-leave (fn [e]
+                                (when (dom/has-class? (.-target e) "code-editor")
+                                  (dom/remove-class! (rum/deref *actions-ref) "opacity-100")))}
+             (cond
+               (nil? inside-portal?) nil
+
+               (or (:slide? config) inside-portal?)
+               (highlight/highlight (str (random-uuid))
+                                    {:class (str "language-" language)
+                                     :data-lang language}
+                                    code)
+
+               :else
+               [:div.ls-code-editor-wrap
+                [:div.code-block-actions.flex.flex-row.gap-1.opacity-0.transition-opacity.ease-in.duration-300
+                 {:ref *actions-ref}
+                 (shui/button
+                  {:variant :text
+                   :size :sm
+                   :class "select-language"
+                   :ref *mode-ref
+                   :containerid (str container-id)
+                   :blockid (str (:block/uuid block))
+                   :on-click (fn [^js e]
+                               (util/stop-propagation e)
+                               (let [target (.-target e)]
+                                 (shui/popup-show! target
+                                                   #(src-lang-picker block
+                                                                     (fn [lang ^js _e]
+                                                                       (when-let [^js cm (util/get-cm-instance (util/rec-get-node target "ls-block"))]
+                                                                         (if-let [mode (get-code-mode-by-lang lang)]
+                                                                           (.setOption cm "mode" mode)
+                                                                           (throw (ex-info "code mode not found"
+                                                                                           {:lang lang})))
+                                                                         (storage/set :latest-code-lang lang)
+                                                                         (db-property-handler/set-block-property!
+                                                                          (:db/id block) :logseq.property.code/lang lang))))
+                                                   {:align :end})))}
+                  (or language "Choose language")
+                  (ui/icon "chevron-down"))
+                 (shui/button
+                  {:variant :text
+                   :size :sm
+                   :on-click (fn [e]
+                               (util/stop-propagation e)
+                               (editor-handler/copy-block-content! block))}
+                  (ui/icon "copy")
+                  "Copy")]
+                (lazy-editor/editor config (str (d/squuid)) attr code options)
+                (let [options (:options options) block (:block config)]
+                  (when (and (= language "clojure") (contains? (set options) ":results"))
+                    (sci/eval-result code block)))])]))))))
 
 (defn ^:large-vars/cleanup-todo markup-element-cp
   [{:keys [html-export?] :as config} item]
@@ -3647,9 +3723,11 @@
       [:pre.pre-wrap-white-space
        (join-lines l)]
       ["Quote" l]
-      (->elem
-       :blockquote
-       (markup-elements-cp config l))
+      (if (config/db-based-graph? (state/get-current-repo))
+        [:div.warning "#+BEGIN_QUOTE is deprecated. Use '/Quote' command instead."]
+        (->elem
+         :blockquote
+         (markup-elements-cp config l)))
       ["Raw_Html" content]
       (when (not html-export?)
         [:div.raw_html {:dangerouslySetInnerHTML
@@ -3668,15 +3746,19 @@
       ["Export" "latex" _options content]
       (if html-export?
         (latex/html-export content true false)
-        (latex/latex (str (d/squuid)) content true false))
+        (if (config/db-based-graph? (state/get-current-repo))
+          [:div.warning "'#+BEGIN_EXPORT latex' is deprecated. Use '/Math block' command instead."]
+          (latex/latex (str (d/squuid)) content true false)))
 
       ["Custom" "query" _options _result content]
-      (try
-        (let [query (common-util/safe-read-map-string content)]
-          (query/custom-query (wrap-query-components config) query))
-        (catch :default e
-          (log/error :read-string-error e)
-          (ui/block-error "Invalid query:" {:content content})))
+      (if (config/db-based-graph? (state/get-current-repo))
+        [:div.warning "#+BEGIN_QUERY is deprecated. Use '/Advanced Query' command instead."]
+        (try
+          (let [query (common-util/safe-read-map-string content)]
+            (query/custom-query (wrap-query-components config) query))
+          (catch :default e
+            (log/error :read-string-error e)
+            (ui/block-error "Invalid query:" {:content content}))))
 
       ["Custom" "note" _options result _content]
       (ui/admonition "note" (markup-elements-cp config result))

+ 25 - 4
src/main/frontend/components/block.css

@@ -888,6 +888,12 @@ html.is-mac {
   }
 }
 
+.positioned-properties.block-below {
+  .block-content.inline {
+    @apply flex;
+  }
+}
+
 .block-tags {
   @apply flex flex-row flex-wrap self-start items-center;
   min-height: 24px;
@@ -936,10 +942,25 @@ html.is-mac {
   margin-top: 8px;
 }
 
-.ls-page-title .positioned-properties {
-  height: 54px;
-}
-
 .ls-page-title .block-tags {
   @apply relative -right-1 min-h-full;
 }
+
+.ls-code-editor-wrap {
+  @apply relative w-full;
+
+  .extensions__code-lang {
+    @apply hidden;
+  }
+
+  > .code-block-actions {
+    @apply absolute right-1 top-1 select-none z-[1] text-xs;
+    button {
+        @apply !py-0 h-4 text-muted-foreground hover:text-foreground text-xs px-1;
+    }
+    svg {
+      width: 14px;
+      height: 14px;
+    }
+  }
+}

+ 2 - 4
src/main/frontend/components/cmdk/core.cljs

@@ -214,10 +214,8 @@
     [:div (loop [content content ;; why recur? because there might be multiple matches
                  result  []]
             (let [[b-cut hl-cut e-cut] (text-util/cut-by content "$pfts_2lqh>$" "$<pfts_2lqh$")
-                  hiccups-add [(when-not (string/blank? b-cut)
-                                 [:span b-cut])
-                               (when-not (string/blank? hl-cut)
-                                 [:mark.p-0.rounded-none hl-cut])]
+                  hiccups-add [[:span b-cut]
+                               [:mark.p-0.rounded-none hl-cut]]
                   hiccups-add (remove nil? hiccups-add)
                   new-result (concat result hiccups-add)]
               (if-not (string/blank? e-cut)

+ 80 - 80
src/main/frontend/components/container.cljs

@@ -168,7 +168,7 @@
   (let [_favorites-updated? (state/sub :favorites/updated?)
         favorite-entities (page-handler/get-favorites)]
     (nav-content-item
-      [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
+     [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
       (ui/icon "star" {:size 16})
       [:strong.flex-1.ml-2 (string/upper-case (t :left-side-bar/nav-favorites))]]
 
@@ -409,32 +409,32 @@
 
     ;; restore size
     (rum/use-layout-effect!
-      (fn []
-        (when-let [width (storage/get :ls-left-sidebar-width)]
-          (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)))
-      [])
+     (fn []
+       (when-let [width (storage/get :ls-left-sidebar-width)]
+         (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)))
+     [])
 
     ;; draggable handler
     (rum/use-effect!
-      (fn []
-        (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
-          (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
-            (-> (js/interact el)
-              (.draggable
+     (fn []
+       (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
+         (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
+           (-> (js/interact el)
+               (.draggable
                 #js {:listeners
                      #js {:move (fn [^js/MouseEvent e]
                                   (when-let [offset (.-left (.-rect e))]
                                     (let [width (.toFixed (max (min offset 460) 240) 2)]
                                       (adjust-size! (str width "px")))))}})
-              (.styleCursor false)
-              (.on "dragstart" (fn []
-                                 (.. sidebar-el -classList (add "is-resizing"))
-                                 (.. el-doc -classList (add "is-resizing-buf"))))
-              (.on "dragend" (fn []
-                               (.. sidebar-el -classList (remove "is-resizing"))
-                               (.. el-doc -classList (remove "is-resizing-buf"))))))
-          #()))
-      [])
+               (.styleCursor false)
+               (.on "dragstart" (fn []
+                                  (.. sidebar-el -classList (add "is-resizing"))
+                                  (.. el-doc -classList (add "is-resizing-buf"))))
+               (.on "dragend" (fn []
+                                (.. sidebar-el -classList (remove "is-resizing"))
+                                (.. el-doc -classList (remove "is-resizing-buf"))))))
+         #()))
+     [])
     [:span.left-sidebar-resizer {:ref *el-ref}]))
 
 (rum/defcs left-sidebar < rum/reactive
@@ -479,7 +479,7 @@
 
      ;; sidebar contents
      (sidebar-nav route-match close-fn left-sidebar-open? enable-whiteboards? srs-open? *closing?
-       @*close-signal (and touch-pending? touching-x-offset))
+                  @*close-signal (and touch-pending? touching-x-offset))
      ;; resizer
      (sidebar-resizer)]))
 
@@ -724,16 +724,16 @@
   []
 
   (rum/use-effect!
-    (fn []
-      (state/set-state! :ui/handbooks-open? false))
-    [])
+   (fn []
+     (state/set-state! :ui/handbooks-open? false))
+   [])
 
   (rum/use-effect!
-    (fn []
-      (let [h #(state/set-state! :ui/help-open? false)]
-        (.addEventListener js/document.body "click" h)
-        #(.removeEventListener js/document.body "click" h)))
-    [])
+   (fn []
+     (let [h #(state/set-state! :ui/help-open? false)]
+       (.addEventListener js/document.body "click" h)
+       #(.removeEventListener js/document.body "click" h)))
+   [])
 
   [:div.cp__sidebar-help-menu-popup
    [:div.list-wrap
@@ -777,58 +777,58 @@
 
 (rum/defc app-context-menu-observer
   < rum/static
-    (mixins/event-mixin
-      (fn [state]
+  (mixins/event-mixin
+   (fn [state]
         ;; fixme: this mixin will register global event listeners on window
         ;; which might cause unexpected issues
-        (mixins/listen state js/window "contextmenu"
-          (fn [^js e]
-            (let [target (gobj/get e "target")
-                  block-el (.closest target ".bullet-container[blockid]")
-                  block-id (some-> block-el (.getAttribute "blockid"))
-                  {:keys [block block-ref]} (state/sub :block-ref/context)
-                  {:keys [page page-entity]} (state/sub :page-title/context)]
-
-              (let [show!
-                    (fn [content]
-                      (shui/popup-show! e
-                                        (fn [{:keys [id]}]
-                                          [:div {:on-click #(shui/popup-hide! id)
-                                                 :data-keep-selection true}
-                                           content])
-                                        {:on-before-hide state/dom-clear-selection!
-                                         :on-after-hide state/state-clear-selection!
-                                         :content-props {:class "w-[280px] ls-context-menu-content"}
-                                         :as-dropdown? true}))
-
-                    handled
-                    (cond
-                      (and page (not block-id))
-                      (do
-                        (show! (cp-content/page-title-custom-context-menu-content page-entity))
-                        (state/set-state! :page-title/context nil))
-
-                      block-ref
-                      (do
-                        (show! (cp-content/block-ref-custom-context-menu-content block block-ref))
-                        (state/set-state! :block-ref/context nil))
+     (mixins/listen state js/window "contextmenu"
+                    (fn [^js e]
+                      (let [target (gobj/get e "target")
+                            block-el (.closest target ".bullet-container[blockid]")
+                            block-id (some-> block-el (.getAttribute "blockid"))
+                            {:keys [block block-ref]} (state/sub :block-ref/context)
+                            {:keys [page page-entity]} (state/sub :page-title/context)]
+
+                        (let [show!
+                              (fn [content]
+                                (shui/popup-show! e
+                                                  (fn [{:keys [id]}]
+                                                    [:div {:on-click #(shui/popup-hide! id)
+                                                           :data-keep-selection true}
+                                                     content])
+                                                  {:on-before-hide state/dom-clear-selection!
+                                                   :on-after-hide state/state-clear-selection!
+                                                   :content-props {:class "w-[280px] ls-context-menu-content"}
+                                                   :as-dropdown? true}))
+
+                              handled
+                              (cond
+                                (and page (not block-id))
+                                (do
+                                  (show! (cp-content/page-title-custom-context-menu-content page-entity))
+                                  (state/set-state! :page-title/context nil))
+
+                                block-ref
+                                (do
+                                  (show! (cp-content/block-ref-custom-context-menu-content block block-ref))
+                                  (state/set-state! :block-ref/context nil))
 
                       ;; block selection
-                      (and (state/selection?) (not (d/has-class? target "bullet")))
-                      (show! (cp-content/custom-context-menu-content))
+                                (and (state/selection?) (not (d/has-class? target "bullet")))
+                                (show! (cp-content/custom-context-menu-content))
 
                       ;; block bullet
-                      (and block-id (parse-uuid block-id))
-                      (let [block (.closest target ".ls-block")]
-                        (when block
-                          (state/clear-selection!)
-                          (state/conj-selection-block! block :down))
-                        (show! (cp-content/block-context-menu-content target (uuid block-id))))
-
-                      :else
-                      false)]
-                (when (not (false? handled))
-                  (util/stop e))))))))
+                                (and block-id (parse-uuid block-id))
+                                (let [block (.closest target ".ls-block")]
+                                  (when block
+                                    (state/clear-selection!)
+                                    (state/conj-selection-block! block :down))
+                                  (show! (cp-content/block-context-menu-content target (uuid block-id))))
+
+                                :else
+                                false)]
+                          (when (not (false? handled))
+                            (util/stop e))))))))
   []
   nil)
 
@@ -908,12 +908,12 @@
 
      [:main.theme-container-inner#app-container-wrapper
       {:class (util/classnames
-                [{:ls-left-sidebar-open left-sidebar-open?
-                  :ls-right-sidebar-open sidebar-open?
-                  :ls-wide-mode wide-mode?
-                  :ls-window-controls window-controls?
-                  :ls-fold-button-on-right fold-button-on-right?
-                  :ls-hl-colored ls-block-hl-colored?}])
+               [{:ls-left-sidebar-open left-sidebar-open?
+                 :ls-right-sidebar-open sidebar-open?
+                 :ls-wide-mode wide-mode?
+                 :ls-window-controls window-controls?
+                 :ls-fold-button-on-right fold-button-on-right?
+                 :ls-hl-colored ls-block-hl-colored?}])
        :on-pointer-up (fn []
                         (when-let [container (gdom/getElement "app-container-wrapper")]
                           (d/remove-class! container "blocks-selection-mode")

+ 5 - 2
src/main/frontend/components/editor.cljs

@@ -3,7 +3,7 @@
             [datascript.impl.entity :as de]
             [dommy.core :as dom]
             [frontend.commands :as commands :refer [*matched-commands]]
-            [frontend.components.datetime :as datetime-comp]
+            [frontend.components.file-based.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
             [frontend.components.title :as title]
@@ -745,7 +745,10 @@
                   ::ref (atom nil)))
    :did-mount (fn [state]
                 (state/set-editor-args! (:rum/args state))
-                state)}
+                state)
+   :will-unmount (fn [state]
+                   (state/set-state! :editor/raw-mode-block nil)
+                   state)}
   (mixins/event-mixin
    (fn [state]
      (mixins/hide-when-esc-or-outside

+ 45 - 0
src/main/frontend/components/file_based/block.cljs

@@ -1,7 +1,11 @@
 (ns frontend.components.file-based.block
   (:require [clojure.string :as string]
+            [frontend.commands :as commands]
+            [frontend.components.file-based.datetime :as datetime-comp]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.file-based.repeated :as repeated]
             [frontend.ui :as ui]
+            [logseq.shui.ui :as shui]
             [frontend.util :as util]
             [frontend.util.file-based.clock :as clock]
             [frontend.util.file-based.drawer :as drawer]
@@ -107,3 +111,44 @@
                    [:div.text-sm.time-spent.ml-1 {:style {:padding-top 3}}
                     [:a.fade-link
                      summary]])]))))
+
+(rum/defc timestamp-editor
+  [ast *show-datapicker?]
+  (let [*trigger-ref (rum/use-ref nil)]
+    (rum/use-effect!
+     (fn []
+       (let [pid (shui/popup-show!
+                  (.closest (rum/deref *trigger-ref) "a")
+                  (datetime-comp/date-picker nil nil (repeated/timestamp->map ast))
+                  {:id :timestamp-editor
+                   :align :start
+                   :root-props {:onOpenChange #(reset! *show-datapicker? %)}
+                   :content-props {:onEscapeKeyDown #(reset! *show-datapicker? false)}})]
+         #(do (shui/popup-hide! pid)
+              (reset! *show-datapicker? false))))
+     [])
+    [:i {:ref *trigger-ref}]))
+
+(rum/defcs timestamp-cp
+  < rum/reactive
+  (rum/local false ::show-datepicker?)
+  [state block typ ast]
+  (let [ts-block-id (get-in (state/sub [:editor/set-timestamp-block]) [:block :block/uuid])
+        _active? (= (get block :block/uuid) ts-block-id)
+        *show-datapicker? (get state ::show-datepicker?)]
+    [:div.flex.flex-col.gap-4.timestamp
+     [:div.text-sm.flex.flex-row
+      [:div.opacity-50.font-medium.timestamp-label
+       (str typ ": ")]
+      [:a.opacity-80.hover:opacity-100
+       {:on-pointer-down (fn [e]
+                           (util/stop e)
+                           (state/clear-editor-action!)
+                           (editor-handler/escape-editing)
+                           (reset! *show-datapicker? true)
+                           (reset! commands/*current-command typ)
+                           (state/set-timestamp-block! {:block block
+                                                        :typ typ}))}
+       [:span.time-start "<"] [:time (repeated/timestamp->text ast)] [:span.time-stop ">"]
+       (when (and _active? @*show-datapicker?)
+         (timestamp-editor ast *show-datapicker?))]]]))

+ 14 - 12
src/main/frontend/components/datetime.cljs → src/main/frontend/components/file_based/datetime.cljs

@@ -1,11 +1,11 @@
-(ns frontend.components.datetime
+(ns frontend.components.file-based.datetime
   (:require [cljs-time.core :as t]
             [clojure.string :as string]
             [frontend.commands :as commands]
             [frontend.components.svg :as svg]
             [frontend.date :as date]
             [frontend.handler.editor :as editor-handler]
-            [frontend.handler.repeated :as repeated]
+            [frontend.handler.file-based.repeated :as repeated]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -151,18 +151,20 @@
                                   (string/lower-case current-command)))
         date (state/sub :date-picker/date)
         select-handler! (fn [^js d]
-                          (let [gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))
-                                journal (date/js-date->journal-title gd)]
+                          ;; d is nil when clicked more than once
+                          (when d
+                            (let [gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))
+                                  journal (date/js-date->journal-title gd)]
                             ;; deadline-or-schedule? is handled in on-submit, not here
-                            (when-not deadline-or-schedule?
+                              (when-not deadline-or-schedule?
                               ;; similar to page reference
-                              (editor-handler/insert-command! dom-id
-                                (page-ref/->page-ref journal)
-                                format
-                                {:command :page-ref})
-                              (state/clear-editor-action!)
-                              (reset! commands/*current-command nil))
-                            (state/set-state! :date-picker/date d)))]
+                                (editor-handler/insert-command! dom-id
+                                                                (page-ref/->page-ref journal)
+                                                                format
+                                                                {:command :page-ref})
+                                (state/clear-editor-action!)
+                                (reset! commands/*current-command nil))
+                              (state/set-state! :date-picker/date d))))]
     [:div#date-time-picker.flex.flex-col.sm:flex-row
      ;; inline container
      [:div.border-red-500

+ 2 - 2
src/main/frontend/components/file_based/query.cljs

@@ -31,7 +31,7 @@
 (rum/defc custom-query-header
   [{:keys [dsl-query?] :as config}
    {:keys [title query] :as q}
-   {:keys [collapsed? result table? current-block view-f page-list? query-error-atom fulltext-query-result-atom]}]
+   {:keys [collapsed? result table? current-block view-f page-list? query-error-atom]}]
   (let [dsl-page-query? (and dsl-query?
                              (false? (:blocks? (query-dsl/parse-query query))))
         full-text-search? (and dsl-query?
@@ -82,4 +82,4 @@
            (query-refresh-button query-time {:full-text-search? full-text-search?
                                              :on-pointer-down (fn [e]
                                                                 (util/stop e)
-                                                                (query-result/trigger-custom-query! config q query-error-atom fulltext-query-result-atom))}))]])]))
+                                                                (query-result/trigger-custom-query! config q query-error-atom (fn [])))}))]])]))

+ 54 - 50
src/main/frontend/components/objects.cljs

@@ -132,33 +132,34 @@
     (when (false? loading?)
       [:div.flex.flex-col.gap-2.mt-2
 
-       [:div.font-medium.opacity-50 "Tagged Nodes"]
-
-       (views/view view-entity {:data data
-                                :set-data! set-data!
-                                :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
-                                                                                   :set-views! set-views!})
-                                :columns columns
-                                :add-new-object! #(add-new-class-object! class set-data!)
-                                :show-add-property? true
-                                :add-property! (fn []
-                                                 (state/pub-event! [:editor/new-property {:block class
-                                                                                          :class-schema? true}]))
-                                :on-delete-rows (fn [table selected-rows]
-                                                  (let [pages (filter ldb/page? selected-rows)
-                                                        blocks (remove ldb/page? selected-rows)]
-                                                    (p/do!
-                                                     (ui-outliner-tx/transact!
-                                                      {:outliner-op :delete-blocks}
-                                                      (when (seq blocks)
-                                                        (outliner-op/delete-blocks! blocks nil))
-                                                      (let [page-ids (map :db/id pages)
-                                                            tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
-                                                        (when (seq tx-data)
-                                                          (outliner-op/transact! tx-data {:outliner-op :save-block}))))
-                                                     (set-data! (get-class-objects class))
-                                                     (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                       (f {})))))})])))
+       (ui/foldable
+        [:div.font-medium.opacity-50 "Tagged Nodes"]
+        (views/view view-entity {:data data
+                                 :set-data! set-data!
+                                 :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
+                                                                                    :set-views! set-views!})
+                                 :columns columns
+                                 :add-new-object! #(add-new-class-object! class set-data!)
+                                 :show-add-property? true
+                                 :add-property! (fn []
+                                                  (state/pub-event! [:editor/new-property {:block class
+                                                                                           :class-schema? true}]))
+                                 :on-delete-rows (fn [table selected-rows]
+                                                   (let [pages (filter ldb/page? selected-rows)
+                                                         blocks (remove ldb/page? selected-rows)]
+                                                     (p/do!
+                                                      (ui-outliner-tx/transact!
+                                                       {:outliner-op :delete-blocks}
+                                                       (when (seq blocks)
+                                                         (outliner-op/delete-blocks! blocks nil))
+                                                       (let [page-ids (map :db/id pages)
+                                                             tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
+                                                         (when (seq tx-data)
+                                                           (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                                      (set-data! (get-class-objects class))
+                                                      (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                                        (f {})))))})
+        {})])))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
   [state class]
@@ -214,30 +215,33 @@
      [])
 
     (when (false? loading?)
-      (views/view view-entity {:data data
-                               :set-data! set-data!
-                               :title-key :views.table/property-nodes
-                               :columns columns
-                               :add-new-object! #(add-new-property-object! property set-data!)
+      (ui/foldable
+       [:div.font-medium.opacity-50 "Nodes with Property"]
+       (views/view view-entity {:data data
+                                :set-data! set-data!
+                                :title-key :views.table/property-nodes
+                                :columns columns
+                                :add-new-object! #(add-new-property-object! property set-data!)
                                ;; TODO: Add support for adding column
-                               :show-add-property? false
-                               :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
-                                                                    (:db/ident property))
-                                                 (fn [table selected-rows]
-                                                   (let [pages (filter ldb/page? selected-rows)
-                                                         blocks (remove ldb/page? selected-rows)]
-                                                     (p/do!
-                                                      (ui-outliner-tx/transact!
-                                                       {:outliner-op :delete-blocks}
-                                                       (when (seq blocks)
-                                                         (outliner-op/delete-blocks! blocks nil))
-                                                       (let [page-ids (map :db/id pages)
-                                                             tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)]
-                                                         (when (seq tx-data)
-                                                           (outliner-op/transact! tx-data {:outliner-op :save-block}))))
-                                                      (set-data! (get-property-related-objects (state/get-current-repo) property))
-                                                      (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                        (f {}))))))}))))
+                                :show-add-property? false
+                                :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
+                                                                     (:db/ident property))
+                                                  (fn [table selected-rows]
+                                                    (let [pages (filter ldb/page? selected-rows)
+                                                          blocks (remove ldb/page? selected-rows)]
+                                                      (p/do!
+                                                       (ui-outliner-tx/transact!
+                                                        {:outliner-op :delete-blocks}
+                                                        (when (seq blocks)
+                                                          (outliner-op/delete-blocks! blocks nil))
+                                                        (let [page-ids (map :db/id pages)
+                                                              tx-data (map (fn [pid] [:db/retract pid (:db/ident property)]) page-ids)]
+                                                          (when (seq tx-data)
+                                                            (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                                       (set-data! (get-property-related-objects (state/get-current-repo) property))
+                                                       (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                                         (f {}))))))})
+       {}))))
 
 ;; Show all nodes containing the given property
 (rum/defcs property-related-objects < rum/reactive db-mixins/query mixins/container-id

+ 16 - 16
src/main/frontend/components/page_menu.cljs

@@ -25,26 +25,26 @@
 (defn- delete-page!
   [page]
   (page-handler/<delete! (:block/uuid page)
-                        (fn []
-                          (notification/show! (str "Page " (:block/title page) " was deleted successfully!")
-                                              :success))
-                        {:error-handler (fn [{:keys [msg]}]
-                                          (notification/show! msg :warning))}))
+                         (fn []
+                           (notification/show! (str "Page " (:block/title page) " was deleted successfully!")
+                                               :success))
+                         {:error-handler (fn [{:keys [msg]}]
+                                           (notification/show! msg :warning))}))
 
 (defn delete-page-confirm!
   [page]
   (when page
     (-> (shui/dialog-confirm!
-          {:title [:h3.text-lg.leading-6.font-medium.flex.gap-2.items-center
-                   [:span.top-1.relative
-                    (shui/tabler-icon "alert-triangle")]
-                   (if (config/db-based-graph? (state/get-current-repo))
-                     (t :page/db-delete-confirmation)
-                     (t :page/delete-confirmation))]
-           :content [:p.opacity-60 (str "- " (:block/title page))]
-           :outside-cancel? true})
-      (p/then #(delete-page! page))
-      (p/catch #()))))
+         {:title [:h3.text-lg.leading-6.font-medium.flex.gap-2.items-center
+                  [:span.top-1.relative
+                   (shui/tabler-icon "alert-triangle")]
+                  (if (config/db-based-graph? (state/get-current-repo))
+                    (t :page/db-delete-confirmation)
+                    (t :page/delete-confirmation))]
+          :content [:p.opacity-60 (str "- " (:block/title page))]
+          :outside-cancel? true})
+        (p/then #(delete-page! page))
+        (p/catch #()))))
 
 (defn ^:large-vars/cleanup-todo page-menu
   [page]
@@ -155,7 +155,7 @@
                :options {:on-click #(commands/exec-plugin-simple-command!
                                      pid (assoc cmd :page page-name) action)}}))
 
-          (when (and db-based? (= (:block/type page) "page"))
+          (when (and db-based? (ldb/internal-page? page))
             {:title (t :page/convert-to-tag)
              :options {:on-click (fn []
                                    (db-page-handler/convert-to-tag! page))}})

+ 5 - 14
src/main/frontend/components/property.cljs

@@ -33,13 +33,6 @@
             [promesa.core :as p]
             [rum.core :as rum]))
 
-(defn- property-type-label
-  [property-type]
-  (case property-type
-    :default
-    "Text"
-    ((comp string/capitalize name) property-type)))
-
 (defn- <add-property-from-dropdown
   "Adds an existing or new property from dropdown. Used from a block or page context."
   [entity property-uuid-or-name schema {:keys [class-schema?]}]
@@ -83,7 +76,7 @@
                                   (when built-in?
                                     db-property-type/internal-built-in-property-types))
                           (map (fn [type]
-                                 {:label (property-type-label type)
+                                 {:label (property-config/property-type-label type)
                                   :value type})))]
     [:div {:class "flex items-center col-span-1"}
      (shui/select
@@ -218,10 +211,7 @@
 
             (and (= :default type)
                  (not (seq (:property/closed-values property))))
-            (p/do!
-             (pv/<create-new-block! block property "")
-             (shui/popup-hide!)
-             (shui/dialog-close!))
+            (pv/<create-new-block! block property "")
 
             (or (not= :default type)
                 (and (= :default type) (seq (:property/closed-values property))))
@@ -339,7 +329,8 @@
                                    block-types (if (and page? (not= block-type :page))
                                                  #{:page block-type}
                                                  #{block-type})]
-                               (or (and (not page?) (contains? #{:block/alias} (:block/title m)))
+                               (or (contains? #{:logseq.property/query} (:db/ident m))
+                                   (and (not page?) (contains? #{:block/alias} (:db/ident m)))
                                    ;; Filters out properties from being in wrong :view-context and :never view-contexts
                                    (and (not= view-context :all) (not (contains? block-types view-context))))))
         property (rum/react *property)
@@ -654,7 +645,7 @@
                           (state/set-selection-blocks! [block])
                           (some-> js/document.activeElement (.blur)))
                         (d/remove-class! target "ls-popup-closed")))}
-       (let [properties' (remove (fn [[k _v]] (contains? #{:logseq.property/icon} k)) full-properties)]
+       (let [properties' (remove (fn [[k _v]] (contains? #{:logseq.property/icon :logseq.property/query} k)) full-properties)]
          (properties-section block (if class-schema? properties properties') opts))
 
        (rum/with-key (new-property block opts) (str id "-add-property"))])))

+ 81 - 79
src/main/frontend/components/property/config.cljs

@@ -38,12 +38,12 @@
   "Create new closed value and returns its block UUID."
   [property item]
   (p/do!
-    (db-property-handler/upsert-closed-value! (:db/ident property) item)
-    (re-init-commands! property)))
+   (db-property-handler/upsert-closed-value! (:db/ident property) item)
+   (re-init-commands! property)))
 
 (defn- loop-focusable-elements!
   ([^js cnt] (loop-focusable-elements! cnt
-               ".ui__button:not([disabled]), .ui__input, .ui__textarea"))
+                                       ".ui__button:not([disabled]), .ui__input, .ui__textarea"))
   ([^js cnt selectors]
    (when-let [els (some-> cnt (.querySelectorAll selectors) (seq))]
      (let [active js/document.activeElement
@@ -51,7 +51,7 @@
            total-len (count els)
            to-idx (cond
                     (or (= -1 current-idx)
-                      (= total-len (inc current-idx)))
+                        (= total-len (inc current-idx)))
                     0
                     :else
                     (inc current-idx))]
@@ -61,13 +61,13 @@
   [property description]
   (if-let [ent (:logseq.property/description property)]
     (db/transact! (state/get-current-repo)
-      [(outliner-core/block-with-updated-at
-         {:db/id (:db/id ent) :block/title description})]
-      {:outliner-op :save-block})
+                  [(outliner-core/block-with-updated-at
+                    {:db/id (:db/id ent) :block/title description})]
+                  {:outliner-op :save-block})
     (when-not (string/blank? description)
       (db-property-handler/set-block-property!
-        (:db/id property)
-        :logseq.property/description description))))
+       (:db/id property)
+       :logseq.property/description description))))
 
 (defn- <create-class-if-not-exists!
   [value]
@@ -126,10 +126,10 @@
                                         (if (= value :no-tag)
                                           (toggle-fn)
                                           (p/let [result (<create-class-if-not-exists! value)
-                                                 value' (or result value)
-                                                 tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]
-                                                 _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
-                                           (when-not multiple-choices? (toggle-fn)))))}]
+                                                  value' (or result value)
+                                                  tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]
+                                                  _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
+                                            (when-not multiple-choices? (toggle-fn)))))}]
 
                  (select/select opts)))]
 
@@ -160,9 +160,9 @@
         description (util/trim-safe (:description form-data))]
 
     (rum/use-effect!
-      (fn []
-        (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
-      [])
+     (fn []
+       (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
+     [])
 
     [:div.ls-property-name-edit-pane.outline-none
      {:on-key-down (fn [^js e] (when (= "Tab" (.-key e))
@@ -171,10 +171,10 @@
       :ref *el}
      [:div.flex.items-center.input-wrap
       (icon-component/icon-picker (:icon form-data)
-        {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
-         :popup-opts {:align "start"}
-         :del-btn? (boolean (:icon form-data))
-         :empty-label "?"})
+                                  {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
+                                   :popup-opts {:align "start"}
+                                   :del-btn? (boolean (:icon form-data))
+                                   :empty-label "?"})
       (shui/input {:ref *input-ref :size "sm" :default-value title :placeholder "name"
                    :disabled disabled? :on-change (fn [^js e] (set-form-data! (assoc form-data :title (util/trim-safe (util/evalue e)))))})]
      [:div.pt-2 (shui/textarea {:placeholder "description" :default-value description
@@ -187,74 +187,74 @@
                       :on-click (fn []
                                   (set-saving! true)
                                   (-> [(db-property-handler/upsert-property!
-                                         (:db/ident property)
-                                         (:block/schema property)
-                                         {:property-name title
-                                          :properties {:logseq.property/icon (:icon form-data)}})
+                                        (:db/ident property)
+                                        (:block/schema property)
+                                        {:property-name title
+                                         :properties {:logseq.property/icon (:icon form-data)}})
                                        (when (not= description (:description (rum/deref *form-data)))
                                          (set-property-description! property description))]
-                                    (p/all)
-                                    (p/then #(set-sub-open! false))
-                                    (p/catch #(shui/toast! (str %) :error))
-                                    (p/finally #(set-saving! false))))}
-          "Save")])]))
+                                      (p/all)
+                                      (p/then #(set-sub-open! false))
+                                      (p/catch #(shui/toast! (str %) :error))
+                                      (p/finally #(set-saving! false))))}
+                     "Save")])]))
 
 (rum/defc choice-base-edit-form
   [own-property block]
   (let [create? (:create? block)
         uuid (:block/uuid block)
         *form-data (rum/use-ref
-                     {:value (or (str (db-property/closed-value-content block)) "")
-                      :icon (:logseq.property/icon block)
-                      :description (or (db-property/property-value-content (:logseq.property/description block)) "")})
+                    {:value (or (str (db-property/closed-value-content block)) "")
+                     :icon (:logseq.property/icon block)
+                     :description (or (db-property/property-value-content (:logseq.property/description block)) "")})
         [form-data, set-form-data!] (rum/use-state (rum/deref *form-data))
         *input-ref (rum/use-ref nil)]
 
     (rum/use-effect!
-      (fn []
-        (when create?
-          (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
-      [])
+     (fn []
+       (when create?
+         (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
+     [])
 
     [:div.ls-base-edit-form
      [:div.flex.items-center.input-wrap
       (icon-component/icon-picker
-        (:icon form-data)
-        {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
-         :empty-label "?"
-         :del-btn? (boolean (:icon form-data))
-         :popup-opts {:align "start"}})
+       (:icon form-data)
+       {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
+        :empty-label "?"
+        :del-btn? (boolean (:icon form-data))
+        :popup-opts {:align "start"}})
 
       (shui/input {:ref *input-ref :size "sm"
                    :default-value (:value form-data)
                    :on-change (fn [^js e] (set-form-data! (assoc form-data :value (util/trim-safe (util/evalue e)))))
                    :placeholder "title"})]
      [:div.pt-2 (shui/textarea
-                  {:placeholder "description" :default-value (:description form-data)
-                   :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
+                 {:placeholder "description" :default-value (:description form-data)
+                  :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
      [:div.pt-2.flex.justify-end
       (let [dirty? (not= (rum/deref *form-data) form-data)]
         (shui/button {:size "sm"
                       :disabled (not dirty?)
                       :on-click (fn []
                                   (-> (<upsert-closed-value! own-property
-                                        (cond-> form-data uuid (assoc :id uuid)))
-                                    (p/then #(shui/popup-hide!))
-                                    (p/catch #(shui/toast! (str %) :error))))
+                                                             (cond-> form-data uuid (assoc :id uuid)))
+                                      (p/then #(shui/popup-hide!))
+                                      (p/catch #(shui/toast! (str %) :error))))
                       :variant (if dirty? :default :secondary)}
-          "Save"))]]))
+                     "Save"))]]))
 
 (defn restore-root-highlight-item!
   [id]
   (js/setTimeout
-    #(some-> (gdom/getElement id) (.focus)) 32))
+   #(some-> (gdom/getElement id) (.focus)) 32))
 
 (rum/defc dropdown-editor-menuitem
   [{:keys [id icon title desc submenu-content item-props sub-content-props disabled? toggle-checked? on-toggle-checked-change]}]
   (let [submenu-content (when-not disabled? submenu-content)
         item-props' (if (and disabled? (:on-select item-props))
-                     (assoc item-props :on-select (fn [] nil))
-                     item-props)
+                      (assoc item-props :on-select (fn [] nil))
+                      item-props)
         [sub-open? set-sub-open!] (rum/use-state false)
         toggle? (boolean? toggle-checked?)
         id1 (str (or id icon (random-uuid)))
@@ -302,27 +302,27 @@
   [property block]
   (let [delete-choice! (fn []
                          (p/do!
-                           (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
-                           (re-init-commands! property)))
+                          (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
+                          (re-init-commands! property)))
         update-icon! (fn [icon]
                        (property-handler/set-block-property!
-                         (state/get-current-repo) (:block/uuid block) :logseq.property/icon
-                         (select-keys icon [:id :type :color])))
+                        (state/get-current-repo) (:block/uuid block) :logseq.property/icon
+                        (select-keys icon [:id :type :color])))
         icon (:logseq.property/icon block)
         value (db-property/closed-value-content block)]
 
     [:li
      (shui/tabler-icon "grip-vertical" {:size 14})
      (shui/button {:size "sm" :variant :outline}
-       (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
-                                         :popup-opts {:align "start"}
-                                         :del-btn? (boolean icon)
-                                         :empty-label "?"}))
+                  (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
+                                                    :popup-opts {:align "start"}
+                                                    :del-btn? (boolean icon)
+                                                    :empty-label "?"}))
      [:strong {:on-click (fn [^js e]
                            (shui/popup-show! (.-target e)
-                             (fn [] (choice-base-edit-form property block))
-                             {:id :ls-base-edit-form
-                              :align "start"}))}
+                                             (fn [] (choice-base-edit-form property block))
+                                             {:id :ls-base-edit-form
+                                              :align "start"}))}
       value]
      [:a.del {:on-click delete-choice!
               :title "Delete this choice"}
@@ -334,7 +334,7 @@
         values' (if uuid-values?
                   (let [values' (map #(db/entity [:block/uuid %]) values)]
                     (->> (util/distinct-by db-property/closed-value-content values')
-                        (map :block/uuid)))
+                         (map :block/uuid)))
                   values)]
     [:div.flex.flex-col.gap-1.w-64.p-4.overflow-y-auto
      {:class "max-h-[50dvh]"}
@@ -433,11 +433,13 @@
          (dropdown-editor-menuitem
           (assoc v :item-props item-props))))]))
 
-(defn- property-type-label
+(defn property-type-label
   [property-type]
   (case property-type
     :default
     "Text"
+    :datetime
+    "DateTime"
     ((comp string/capitalize name) property-type)))
 
 (defn- handle-delete-property!
@@ -464,12 +466,12 @@
   (let [handle-select! (fn [^js e]
                          (when-let [v (some-> (.-target e) (.-dataset) (.-value))]
                            (p/do!
-                             (db-property-handler/upsert-property!
+                            (db-property-handler/upsert-property!
                              (:db/ident property)
                              (assoc (:block/schema property) :type (keyword v))
                              {})
-                             (set-sub-open! false)
-                             (restore-root-highlight-item! id))))
+                            (set-sub-open! false)
+                            (restore-root-highlight-item! id))))
         item-props {:on-select handle-select!}
         schema-types (->> db-property-type/user-built-in-property-types
                           (map (fn [type]
@@ -546,10 +548,10 @@
                                         (update-cardinality-fn))))}))
 
      (let [group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                                (not
-                                  (and (= :default (get-in property [:block/schema :type]))
-                                    (empty? (:property/closed-values property))
-                                    (contains? #{nil :properties} (:position property-schema)))))
+                                   (not
+                                    (and (= :default (get-in property [:block/schema :type]))
+                                         (empty? (:property/closed-values property))
+                                         (contains? #{nil :properties} (:position property-schema)))))
                           (let [position (:position property-schema)]
                             (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
                                                        :item-props {:class "ui__position-trigger-item"}
@@ -558,8 +560,8 @@
                         (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
                           (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:hide? property-schema))
                                                      :on-toggle-checked-change #(db-property-handler/upsert-property! (:db/ident property)
-                                                                                  (assoc property-schema :hide? %) {})}))]
-                    (remove nil?))]
+                                                                                                                      (assoc property-schema :hide? %) {})}))]
+                       (remove nil?))]
        (when (> (count group') 0)
          (cons (shui/dropdown-menu-separator) group')))
 
@@ -567,11 +569,11 @@
        [:<>
         (shui/dropdown-menu-separator)
         (dropdown-editor-menuitem
-          {:icon :share-3 :title "Go to this property" :desc ""
-           :item-props {:class "opacity-90 focus:opacity-100"
-                        :on-select (fn []
-                                     (shui/popup-hide-all!)
-                                     (route-handler/redirect-to-page! (:block/uuid property)))}})])
+         {:icon :share-3 :title "Go to this property" :desc ""
+          :item-props {:class "opacity-90 focus:opacity-100"
+                       :on-select (fn []
+                                    (shui/popup-hide-all!)
+                                    (route-handler/redirect-to-page! (:block/uuid property)))}})])
 
      (when (and owner-block
                 (not (and
@@ -579,7 +581,7 @@
                       (contains? #{:logseq.property/parent} (:db/ident property)))))
        (dropdown-editor-menuitem
         {:id :delete-property :icon :x
-         :title (if class-schema? "Delete property from tag" "Delete from from node")
+         :title (if class-schema? "Delete property from tag" "Delete property from node")
          :desc "" :disabled? false
          :item-props {:class "opacity-60 focus:!text-red-rx-09 focus:opacity-100"
                       :on-select (fn [^js e]
@@ -612,4 +614,4 @@
   (let [property (db/sub-block (:db/id property*))
         values (rum/react (::values state))]
     (when-not (= :loading values)
-        (dropdown-editor-impl property owner-block values opts))))
+      (dropdown-editor-impl property owner-block values opts))))

+ 12 - 61
src/main/frontend/components/property/value.cljs

@@ -4,7 +4,6 @@
             [datascript.impl.entity :as de]
             [dommy.core :as d]
             [frontend.components.icon :as icon-component]
-            [frontend.components.query.builder :as query-builder-component]
             [frontend.components.select :as select]
             [frontend.components.title :as title]
             [frontend.config :as config]
@@ -29,15 +28,13 @@
             [goog.dom :as gdom]
             [goog.functions :refer [debounce]]
             [lambdaisland.glogi :as log]
-            [logseq.common.util :as common-util]
             [logseq.common.util.macro :as macro-util]
             [logseq.db :as ldb]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.modules.outliner.op :as outliner-op]))
+            [rum.core :as rum]))
 
 (rum/defc property-empty-btn-value
   [property & opts]
@@ -109,8 +106,9 @@
 (defn <create-new-block!
   [block property value & {:keys [edit-block?]
                            :or {edit-block? true}}]
-  (shui/popup-hide!)
-  (shui/dialog-close!)
+  (when-not (get-in property [:block/schema :hide?])
+    (shui/popup-hide!)
+    (shui/dialog-close!))
   (p/let [block
           (if (and (= :default (get-in property [:block/schema :type]))
                    (not (db-property/many? property)))
@@ -243,7 +241,7 @@
        :id @*ident
        :del-btn? del-btn?
        :on-delete on-delete
-       :on-select select-handler!}
+       :on-day-click select-handler!}
        initial-month
        (assoc :default-month initial-month)))))
 
@@ -332,7 +330,7 @@
                                       (property-handler/set-block-property! repo (:block/uuid block)
                                                                             (:db/ident property)
                                                                             (if (map? value) (:db/id value) value)))
-                         :del-btn? (some-> value (:block/title) (boolean))
+                         :del-btn? (some? value)
                          :on-delete (fn []
                                       (property-handler/set-block-property! repo (:block/uuid block)
                                                                             (:db/ident property) nil)
@@ -352,7 +350,7 @@
                    (and (= :logseq.property/parent (:db/ident property))
                         (ldb/class? block)))
         ;; Note: property and other types shouldn't be converted to class
-        page? (= "page" (:block/type page-entity))]
+        page? (ldb/internal-page? page-entity)]
     (cond
       ;; page not exists or page exists but not a page type
       (or (nil? id) (and class? (not page?)))
@@ -466,11 +464,11 @@
                                  (conj (:block/uuid block))) ; break cycle
                  options (if (ldb/class? block)
                            (model/get-all-classes repo)
-                           (cond->>
-                            (->> (model/get-all-pages repo)
-                                 (remove (fn [e] (or (ldb/built-in? e) (ldb/property? e)))))
-                             (contains? #{"property" "page"} (:block/type block))
-                             (remove ldb/class?)))
+                           (when (ldb/internal-page? block)
+                             (cond->>
+                              (->> (model/get-all-pages repo)
+                                   (filter ldb/internal-page?)
+                                   (remove ldb/built-in?)))))
                  excluded-options (remove (fn [e] (contains? exclude-ids (:block/uuid e))) options)]
              excluded-options)
 
@@ -715,50 +713,6 @@
         :on-click (fn [] (<create-new-block! block property ""))}
        (property-empty-btn-value property)])))
 
-(rum/defcs query-cp <
-  (rum/local false ::show-setting?)
-  [state block property v-block]
-  (let [result (common-util/safe-read-string (:block/title v-block))
-        advanced-query? (or (and (map? result) (:query result))
-                            (string/starts-with? (string/triml (:block/title v-block)) "{"))]
-    [:div.flex.flex-1.flex-row.gap-1.justify-between
-     [:div.flex.flex-1 (property-normal-block-value block property v-block)]
-     (when-not advanced-query?
-       (shui/button
-        {:variant :ghost
-         :size :sm
-         :class "jtrigger px-1 text-muted-foreground"
-         :title "Update query"
-         :on-click (fn [e]
-                     (shui/popup-show!
-                      (.-target e)
-                      (fn []
-                        (let [block (db/entity (:db/id v-block))
-                              query (:block/title block)]
-                          [:div.p-4.h-64.flex.flex-col.gap-4 {:style {:width "42rem"}}
-                           [:div.flex.flex-1
-                            (query-builder-component/builder query {:property property
-                                                                    :block block})]
-
-                           [:div
-                            (shui/button
-                             {:variant :ghost
-                              :size :sm
-                              :class "text-muted-foreground"
-                              :on-click (fn []
-                                          (p/do!
-                                           (ui-outliner-tx/transact!
-                                            {:outliner-op :save-block}
-                                            (db-property-handler/set-block-properties! (:db/id block)
-                                                                                       {:logseq.property.node/type :code
-                                                                                        :logseq.property.code/mode "clojure"})
-                                            (outliner-op/save-block! {:db/id (:db/id block) :block/title ""}))
-
-                                           (shui/popup-hide!)))}
-                             "Switch to advanced query")]]))
-                      {:align :end}))}
-        (ui/icon "settings" {:size 18})))]))
-
 (rum/defcs property-block-value < rum/reactive db-mixins/query
   {:init (fn [state]
            (let [block (first (:rum/args state))]
@@ -775,9 +729,6 @@
                                "Invalid block value, please delete the current property."]]
           (when v-block
             (cond
-              (= (:db/ident property) :logseq.property/query)
-              (query-cp block property v-block)
-
               (:block/page v-block)
               (property-normal-block-value block property v-block)
 

+ 87 - 76
src/main/frontend/components/query.cljs

@@ -124,10 +124,70 @@
                           (:block/collapsed? current-block)))]
     collapsed?'))
 
-(rum/defcs custom-query* < rum/reactive rum/static db-mixins/query
-  (rum/local nil ::query-result-atom)
+(rum/defc custom-query* < rum/reactive db-mixins/query
+  [{:keys [*query-error db-graph? dsl-query? built-in-query? table? current-block] :as config}
+   {:keys [builder query view collapsed?] :as q}
+   *result]
+  (let [collapsed?' (:collapsed? config)
+        result' (rum/react *result)]
+    (when (seq result')
+      (let [result (when *result (query-result/transform-query-result config q result'))
+            ;; Args for displaying query header and results
+            view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
+            view-f (and view-fn (sci/eval-string (pr-str view-fn)))
+            page-list? (and (seq result) (some? (:block/name (first result))))
+            opts {:query-error-atom *query-error
+                  :current-block current-block
+                  :table? table?
+                  :view-f view-f
+                  :page-list? page-list?
+                  :result result
+                  :group-by-page? (query-result/get-group-by-page q {:table? table?})}]
+        (if (:custom-query? config)
+      ;; Don't display recursive results when query blocks are a query result
+          [:code (if dsl-query? (str "Results for " (pr-str query)) "Advanced query results")]
+          (when-not (and built-in-query? (empty? result))
+            [:div.custom-query (get config :attr {})
+             (when (and (not db-graph?) (not built-in-query?))
+               (file-query/custom-query-header config
+                                               q
+                                               {:query-error-atom *query-error
+                                                :current-block current-block
+                                                :table? table?
+                                                :view-f view-f
+                                                :page-list? page-list?
+                                                :result result
+                                                :collapsed? collapsed?'}))
+
+             (when (and dsl-query? builder) builder)
+
+             (if built-in-query?
+               [:div {:style {:margin-left 2}}
+                (ui/foldable
+                 (query-title config (:title q) {:result-count (count result)})
+                 (fn []
+                   (custom-query-inner config q opts))
+                 {:default-collapsed? collapsed?
+                  :title-trigger? true})]
+               (when-not (:table? config)
+                 [:div.bd
+                  (when-not collapsed?'
+                    (custom-query-inner config q opts))]))]))))))
+
+(rum/defc trigger-custom-query
+  [config q]
+  (let [[result set-result!] (rum/use-state nil)]
+    (rum/use-effect!
+     (fn []
+       (query-result/trigger-custom-query! config q (:*query-error config) set-result!))
+     [q])
+    (when (util/atom? result)
+      (custom-query* config q result))))
+
+(rum/defcs custom-query < rum/static
   {:init (fn [state]
-           (let [[{:keys [dsl-query? db-graph? built-in-query?] :as config}
+           (let [db-graph? (config/db-based-graph? (state/get-current-repo))
+                 [{:keys [dsl-query? built-in-query?] :as config}
                   {:keys [collapsed?]}] (:rum/args state)]
              ;; collapsed? not needed for db graphs
              (when (not db-graph?)
@@ -135,78 +195,29 @@
                  (when collapsed?
                    (editor-handler/collapse-block! (or (:block/uuid (:block config))
                                                        (:block/uuid config)))))))
-           (assoc state :query-error (atom nil)
-                  :fulltext-query-result (atom nil)))}
-  [state {:keys [db-graph? dsl-query? built-in-query?] :as config} {:keys [builder query view collapsed?] :as q}]
-  (let [*query-result-atom (::query-result-atom state)
-        *query-error (:query-error state)
-        *fulltext-query-result (:fulltext-query-result state)
-        current-block-uuid (or (:block/uuid (:block config))
-                               (:block/uuid config))
-        current-block (db/entity [:block/uuid current-block-uuid])
-        ;; Get query result
-        collapsed?' (calculate-collapsed? current-block current-block-uuid {:collapsed? (if-not db-graph? collapsed? false)})
-        built-in-collapsed? (and collapsed? built-in-query?)
-        table? (when-not db-graph?
-                 (or (get-in current-block [:block/properties :query-table])
-                     (and (string? query) (string/ends-with? (string/trim query) "table"))))
-        result (when (or built-in-collapsed? (not collapsed?'))
-                 (or @*query-result-atom
-                     (let [result (query-result/get-query-result config q *query-error *fulltext-query-result current-block-uuid {:table? table?})]
-                       (reset! *query-result-atom result)
-                       result)))
-        ;; Args for displaying query header and results
-        view-fn (if (keyword? view) (get-in (state/sub-config) [:query/views view]) view)
-        view-f (and view-fn (sci/eval-string (pr-str view-fn)))
-        page-list? (and (seq result)
-                        (some? (:block/name (first result))))
-        opts {:query-error-atom *query-error
-              :current-block current-block
-              :table? table?
-              :view-f view-f
-              :page-list? page-list?
-              :result result
-              :group-by-page? (query-result/get-group-by-page q {:table? table?})}]
-    (if (:custom-query? config)
-      ;; Don't display recursive results when query blocks are a query result
-      [:code (if dsl-query? (str "Results for " (pr-str query)) "Advanced query results")]
-      (when-not (and built-in-query? (empty? result))
-        [:div.custom-query (get config :attr {})
-         (when (and (not db-graph?) (not built-in-query?))
-           (file-query/custom-query-header config
-                                           q
-                                           {:query-error-atom *query-error
-                                            :fulltext-query-result-atom *fulltext-query-result
-                                            :current-block current-block
-                                            :table? table?
-                                            :view-f view-f
-                                            :page-list? page-list?
-                                            :result result
-                                            :collapsed? collapsed?'}))
-
-         (when (and dsl-query? builder) builder)
-
-         (if built-in-query?
-           [:div {:style {:margin-left 2}}
-            (ui/foldable
-             (query-title config (:title q) {:result-count (count result)})
-             (fn []
-               (custom-query-inner config q opts))
-             {:default-collapsed? collapsed?
-              :title-trigger? true})]
-           (when-not (:table? config)
-             [:div.bd
-              (when-not collapsed?'
-                (custom-query-inner config q opts))]))]))))
-
-(rum/defcs custom-query < rum/static
-  [state config q]
+           (assoc state :query-error (atom nil)))}
+  [state {:keys [built-in-query?] :as config}
+   {:keys [query collapsed?] :as q}]
   (ui/catch-error
    (ui/block-error "Query Error:" {:content (:query q)})
-   (ui/lazy-visible
-    (fn []
-      (custom-query* (merge config
-                            {:db-graph? (config/db-based-graph? (state/get-current-repo))
-                             :built-in-query? (built-in-custom-query? (:title q))})
-                     q))
-    {:debug-id q})))
+   (let [*query-error (:query-error state)
+         db-graph? (config/db-based-graph? (state/get-current-repo))
+         current-block-uuid (or (:block/uuid (:block config))
+                                (:block/uuid config))
+         current-block (db/entity [:block/uuid current-block-uuid])
+        ;; Get query result
+         collapsed?' (calculate-collapsed? current-block current-block-uuid {:collapsed? (if-not db-graph? collapsed? false)})
+         built-in-collapsed? (and collapsed? built-in-query?)
+         table? (when-not db-graph?
+                  (or (get-in current-block [:block/properties :query-table])
+                      (and (string? query) (string/ends-with? (string/trim query) "table"))))
+         config' (assoc config
+                        :db-graph? db-graph?
+                        :current-block current-block
+                        :current-block-uuid current-block-uuid
+                        :collapsed? collapsed?'
+                        :table? table?
+                        :built-in-query? (built-in-custom-query? (:title q))
+                        :*query-error *query-error)]
+     (when (or built-in-collapsed? (not collapsed?'))
+       (trigger-custom-query config' q)))))

+ 23 - 18
src/main/frontend/components/query/builder.cljs

@@ -344,15 +344,15 @@
   (shui/button
    {:class "jtrigger !px-1 h-6 add-filter text-muted-foreground"
     :size :sm
-    :variant :ghost
-    :title "Add clause"
+    :variant :outline
     :on-pointer-down util/stop-propagation
     :on-click (fn [^js e]
                 (shui/popup-show! (.-target e)
                                   (fn [{:keys [id]}]
                                     (picker *find *tree loc clause {:toggle-fn #(shui/popup-hide! id)}))
                                   {:align :start}))}
-   (ui/icon "plus" {:size 12})))
+   (ui/icon "plus" {:size 14})
+   (when (= [0] loc) "Filter")))
 
 (declare clauses-group)
 
@@ -361,7 +361,7 @@
   (let [f (first clause)]
     (cond
       (string? clause)
-      (str "search: " clause)
+      (str "Search: " clause)
 
       (= (keyword f) :page-ref)
       (page-ref/->page-ref (second clause))
@@ -527,10 +527,17 @@
       q-str
       (str "\"" q-str "\""))))
 
+(defn- get-q
+  [block]
+  (sanitize-q (or (:file-version/query-macro-title block)
+                  (:block/title block)
+                  "")))
+
 (rum/defcs builder <
   (rum/local nil ::find)
   {:init (fn [state]
-           (let [q-str (sanitize-q (first (:rum/args state)))
+           (let [block (first (:rum/args state))
+                 q-str (get-q block)
                  query (common-util/safe-read-string
                         query-dsl/custom-readers
                         (query-dsl/pre-transform-query q-str))
@@ -544,10 +551,9 @@
                           :else
                           [:and])
                  tree (query-builder/from-dsl query')
-                 *tree (atom tree)
-                 config (last (:rum/args state))]
+                 *tree (atom tree)]
              (add-watch *tree :updated (fn [_ _ _old _new]
-                                         (when-let [block (:block config)]
+                                         (when block
                                            (let [q (if (= [:and] @*tree)
                                                      ""
                                                      (let [result (query-builder/->dsl @*tree)]
@@ -555,17 +561,16 @@
                                                          (util/format "\"%s\"" result)
                                                          (str result))))
                                                  repo (state/get-current-repo)
-                                                 block (db/pull [:block/uuid (:block/uuid block)])]
-                                             (when block
-                                               (if (:property config)
-                                                 (editor-handler/save-block! repo (:block/uuid block) q)
-                                                 (let [content (string/replace (:block/title block)
-                                                                               #"\{\{query[^}]+\}\}"
-                                                                               (util/format "{{query %s}}" q))]
-                                                   (editor-handler/save-block! repo (:block/uuid block) content))))))))
+                                                 block (db/entity [:block/uuid (:block/uuid block)])]
+                                             (if (config/db-based-graph? (state/get-current-repo))
+                                               (editor-handler/save-block! repo (:block/uuid block) q)
+                                               (let [content (string/replace (:block/title block)
+                                                                             #"\{\{query[^}]+\}\}"
+                                                                             (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 (sanitize-q (first (:rum/args state)))
+                 (let [q-str (get-q (first (:rum/args state)))
                        blocks-query? (:blocks? (query-dsl/parse-query q-str))
                        find-mode (cond
                                    blocks-query?
@@ -576,7 +581,7 @@
                                    nil)]
                    (when find-mode (reset! (::find state) find-mode))
                    state))}
-  [state _query _config]
+  [state _block _option]
   (let [*find (::find state)
         *tree (::tree state)]
     [:div.cp__query-builder

+ 5 - 1
src/main/frontend/components/query/builder.css

@@ -3,6 +3,10 @@
 
     &-filter {
         @apply flex flex-row flex-wrap items-center gap-1;
+        svg {
+          width: 14px;
+          height: 14px;
+        }
     }
 
     .cp__select-main {
@@ -51,7 +55,7 @@
     }
 
     .clause-bracket {
-        @apply text-2xl font-thin opacity-30;
+        @apply font-bold opacity-50;
         font-family: "Inter";
     }
 

+ 40 - 52
src/main/frontend/components/query/result.cljs

@@ -1,61 +1,53 @@
 (ns frontend.components.query.result
   "Query result related functionality for query components"
-  (:require [frontend.db.utils :as db-utils]
-            [frontend.search :as search]
+  (:require [clojure.string :as string]
             [frontend.db :as db]
-            [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-custom :as query-custom]
+            [frontend.db.query-dsl :as query-dsl]
             [frontend.db.query-react :as query-react]
+            [frontend.db.utils :as db-utils]
+            [frontend.modules.outliner.tree :as tree]
+            [frontend.search :as search]
             [frontend.state :as state]
+            [frontend.template :as template]
             [logseq.common.util :as common-util]
-            [frontend.util :as util]
-            [clojure.string :as string]
-            [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.modules.outliner.tree :as tree]
-            [frontend.template :as template]))
+            [promesa.core :as p]))
 
 (defn trigger-custom-query!
-  [config query *query-error *fulltext-query-result]
+  [config query *query-error set-result!]
   (let [repo (state/get-current-repo)
         current-block-uuid (or (:block/uuid (:block config))
                                (:block/uuid config))
-        _ (reset! *query-error nil)
-        query-atom (try
-                     (cond
-                       (:dsl-query? config)
-                       (let [q (:query query)
-                             form (common-util/safe-read-string q)]
-                         (cond
-                           (and (symbol? form)
+        _ (reset! *query-error nil)]
+    (try
+      (cond
+        (:dsl-query? config)
+        (let [q (:query query)
+              form (common-util/safe-read-string q)]
+          (cond
+            (and (symbol? form)
                                 ;; Queries only containgin template should trigger a query
-                                (not (re-matches template/template-re (string/trim q))))
-                           (atom nil)
+                 (not (re-matches template/template-re (string/trim q))))
+            nil
 
-                           (re-matches #"\".*\"" q) ; full-text search
-                           (do
-                             (p/let [blocks (search/block-search repo (string/trim form) {:limit 30})]
-                               (when (seq blocks)
-                                 (let [result (->> blocks
-                                                   (keep (fn [b]
-                                                           (when-not (= (:block/uuid b) current-block-uuid)
-                                                             [:block/uuid (:block/uuid b)])))
-                                                  ;; Why pull-many here instead of `d/entity`?
-                                                   (db/pull-many (state/get-current-repo) '[*])
-                                                   (remove nil?))]
-                                   (reset! *fulltext-query-result result))))
-                             *fulltext-query-result)
+            (re-matches #"\".*\"" q) ; full-text search
+            (p/let [blocks (search/block-search repo (string/trim form) {:limit 30})]
+              (when (seq blocks)
+                (let [result (->> blocks
+                                  (keep (fn [b]
+                                          (when-not (= (:block/uuid b) current-block-uuid)
+                                            (db/entity [:block/uuid (:block/uuid b)])))))]
+                  (set-result! (atom result)))))
 
-                           :else
-                           (query-dsl/query (state/get-current-repo) q {:cards? (:cards? config)})))
+            :else
+            (set-result! (query-dsl/query (state/get-current-repo) q {:cards? (:cards? config)}))))
 
-                       :else
-                       (query-custom/custom-query query {:current-block-uuid current-block-uuid}))
-                     (catch :default e
-                       (reset! *query-error e)
-                       (atom nil)))]
-    (or query-atom
-        (atom nil))))
+        :else
+        (set-result! (query-custom/custom-query query {:current-block-uuid current-block-uuid
+                                                       ;; FIXME: Remove this temporary workaround for reactivity not working
+                                                       :use-cache? false})))
+      (catch :default e
+        (reset! *query-error e)))))
 
 (defn get-group-by-page [{:keys [result-transform query] :as query-m}
                          {:keys [table?]}]
@@ -65,13 +57,10 @@
          (and (not result-transform)
               (not (and (string? query) (string/includes? query "(by-page false)")))))))
 
-(defn get-query-result
-  "Fetches a query's result, transforms it as needed and saves the result into
-  an atom that is passed in as an argument"
-  [config query-m *query-error *fulltext-query-result current-block-uuid options]
-  (let [query-atom (trigger-custom-query! config query-m *query-error *fulltext-query-result)
-        query-result (and query-atom (rum/react query-atom))
-        ;; exclude the current one, otherwise it'll loop forever
+(defn transform-query-result
+  "Transforms a query result if query conditions and config indicate a transformation"
+  [{:keys [current-block-uuid table?] :as config} query-m query-result]
+  (let [;; exclude the current one, otherwise it'll loop forever
         remove-blocks (if current-block-uuid [current-block-uuid] nil)
         transformed-query-result (when query-result
                                    (let [result (query-react/custom-query-result-transform query-result remove-blocks query-m)]
@@ -80,7 +69,7 @@
                                          (get query-m :remove-block-children? true)
                                          tree/filter-top-level-blocks)
                                        result)))
-        group-by-page? (get-group-by-page query-m options)
+        group-by-page? (get-group-by-page query-m {:table? table?})
         result (if (and group-by-page? (:block/uuid (first transformed-query-result)))
                  (let [result (db-utils/group-by-page transformed-query-result)]
                    (if (map? result)
@@ -89,5 +78,4 @@
                  transformed-query-result)]
     (when-let [query-result (:query-result config)]
       (reset! query-result result))
-    (when query-atom
-      (util/safe-with-meta result (meta @query-atom)))))
+    result))

+ 1 - 3
src/main/frontend/components/query/view.cljs

@@ -4,7 +4,6 @@
             [frontend.db :as db]
             [logseq.db :as ldb]
             [rum.core :as rum]
-            [frontend.util :as util]
             [frontend.mixins :as mixins]))
 
 (defn- columns
@@ -36,8 +35,7 @@
   (let [*result (::result state)
         result' (or @*result (init-result result view-entity))
         columns' (columns (assoc config :container-id (::container-id state)) result')]
-    [:div.query-result.w-full.mt-1
-     {:on-pointer-down util/stop-propagation}
+    [:div.query-result.w-full
      (views/view view-entity
                  {:title-key :views.table/live-query-title
                   :data result'

+ 12 - 12
src/main/frontend/components/select.cljs

@@ -133,22 +133,22 @@
                              {:item-render       (or item-cp (fn [result chosen?]
                                                                (render-item result chosen? multiple-choices? *selected-choices)))
                               :class             "cp__select-results"
-                              :on-chosen         (fn [raw-chosen]
+                              :on-chosen         (fn [raw-chosen e]
                                                    (reset! input "")
                                                    (let [chosen (extract-chosen-fn raw-chosen)]
                                                      (if multiple-choices?
                                                        (if (selected-choices chosen)
                                                          (do
                                                            (swap! *selected-choices disj chosen)
-                                                           (when on-chosen (on-chosen chosen false @*selected-choices)))
+                                                           (when on-chosen (on-chosen chosen false @*selected-choices e)))
                                                          (do
                                                            (swap! *selected-choices conj chosen)
-                                                           (when on-chosen (on-chosen chosen true @*selected-choices))))
+                                                           (when on-chosen (on-chosen chosen true @*selected-choices e))))
                                                        (do
                                                          (when (and close-modal? (not multiple-choices?))
                                                            (state/close-modal!))
                                                          (when on-chosen
-                                                           (on-chosen chosen true @*selected-choices))))))
+                                                           (on-chosen chosen true @*selected-choices e))))))
                               :empty-placeholder (empty-placeholder t)})]
 
                            (when (and multiple-choices? (fn? on-apply))
@@ -206,7 +206,7 @@
                           [:div.mb-2 (t :select.graph/empty-placeholder-description)]
                           (ui/button
                            (t :select.graph/add-graph)
-                            :href (rfe/href :graphs)
+                           :href (rfe/href :graphs)
                            :on-click state/close-modal!)])}
    :graph-remove
    {:items-fn (fn []
@@ -223,14 +223,14 @@
    {:items-fn (fn []
                 (let [current-repo (state/get-current-repo)]
                   (->> (state/get-repos)
-                      (remove (fn [{:keys [url]}]
+                       (remove (fn [{:keys [url]}]
                                 ;; Can't replace current graph as ui wouldn't reload properly
-                                (or (= url current-repo) (not (config/db-based-graph? url)))))
-                      (map (fn [{:keys [url] :as original-graph}]
-                             {:value (text-util/get-graph-name-from-path url)
-                              :id (config/get-repo-dir url)
-                              :graph url
-                              :original-graph original-graph})))))
+                                 (or (= url current-repo) (not (config/db-based-graph? url)))))
+                       (map (fn [{:keys [url] :as original-graph}]
+                              {:value (text-util/get-graph-name-from-path url)
+                               :id (config/get-repo-dir url)
+                               :graph url
+                               :original-graph original-graph})))))
     :on-chosen #(dev-common-handler/import-chosen-graph (:graph %))}})
 
 (rum/defc select-modal < rum/reactive

+ 33 - 32
src/main/frontend/components/theme.cljs

@@ -20,14 +20,14 @@
   []
   (let [*el (rum/use-ref nil)]
     (rum/use-effect!
-      (fn []
-        (when-let [el (rum/deref *el)]
-          (let [w (- (.-offsetWidth el) (.-clientWidth el))
-                c "custom-scrollbar"
-                l (.-classList js/document.documentElement)]
-            (if (or (not util/mac?) (> w 2))
-              (.add l c) (.remove l c)))))
-      [])
+     (fn []
+       (when-let [el (rum/deref *el)]
+         (let [w (- (.-offsetWidth el) (.-clientWidth el))
+               c "custom-scrollbar"
+               l (.-classList js/document.documentElement)]
+           (if (or (not util/mac?) (> w 2))
+             (.add l c) (.remove l c)))))
+     [])
     [:div.fixed.w-16.h-16.overflow-scroll.opacity-0
      {:ref   *el
       :class "top-1/2 -left-1/2 z-[-999]"}]))
@@ -55,26 +55,26 @@
 
     ;; theme color
     (rum/use-effect!
-      #(some-> js/document.documentElement
-         (.setAttribute "data-color"
-           (or accent-color "logseq")))
-      [accent-color])
+     #(some-> js/document.documentElement
+              (.setAttribute "data-color"
+                             (or accent-color "logseq")))
+     [accent-color])
 
     (rum/use-effect!
-      #(some-> js/document.documentElement
-         (.setAttribute "data-font" (or editor-font "default")))
-      [editor-font])
+     #(some-> js/document.documentElement
+              (.setAttribute "data-font" (or editor-font "default")))
+     [editor-font])
 
     (rum/use-effect!
      #(let [doc js/document.documentElement]
         (.setAttribute doc "lang" preferred-language)))
 
     (rum/use-effect!
-      #(js/setTimeout
-         (fn [] (when-not @*once-theme-loaded?
-                  (ipc/ipc :theme-loaded)
-                  (vreset! *once-theme-loaded? true))) 100) ; Wait for the theme to be applied
-      [])
+     #(js/setTimeout
+       (fn [] (when-not @*once-theme-loaded?
+                (ipc/ipc :theme-loaded)
+                (vreset! *once-theme-loaded? true))) 100) ; Wait for the theme to be applied
+     [])
 
     (rum/use-effect!
      #(when (and restored-sidebar?
@@ -128,23 +128,24 @@
      [system-theme?])
 
     (rum/use-effect!
-      (fn []
-        (if settings-open?
-          (shui/dialog-open!
-            (fn [] [:div.settings-modal (settings/settings settings-open?)])
-            {:label "app-settings"
-             :align :top
-             :content-props {:onOpenAutoFocus #(.preventDefault %)}
-             :id :app-settings})
-          (shui/dialog-close! :app-settings)))
-      [settings-open?])
+     (fn []
+       (if settings-open?
+         (shui/dialog-open!
+          (fn [] [:div.settings-modal (settings/settings settings-open?)])
+          {:label "app-settings"
+           :align :top
+           :content-props {:onOpenAutoFocus #(.preventDefault %)}
+           :id :app-settings})
+         (shui/dialog-close! :app-settings)))
+     [settings-open?])
 
     (rum/use-effect!
      #(storage/set :file-sync/onboarding-state onboarding-state)
      [onboarding-state])
 
-    [:div.theme-container
-     {:on-click on-click}
+    [:div#root-container.theme-container
+     {:on-click on-click
+      :tab-index -1}
      child
 
      (pdf/default-embed-playground)

+ 4 - 0
src/main/frontend/components/theme.css

@@ -151,6 +151,10 @@ main.ls-fold-button-on-right {
   }
 }
 
+#root-container.theme-container {
+  @apply outline-none;
+}
+
 main.theme-container-inner {
   --left-sidebar-bg-color: var(--lx-gray-02, hsl(var(--secondary, var(--rx-gray-03-hsl))));
 }

+ 7 - 1
src/main/frontend/components/views.cljs

@@ -225,8 +225,14 @@
   [column sized-columns]
   (let [id (:id column)
         size (get sized-columns id)]
-    (if (number? size)
+    (cond
+      (number? size)
       size
+
+      (= id :logseq.property/query)
+      400
+
+      :else
       (case id
         :select 32
         :add-property 160

+ 6 - 6
src/main/frontend/db/async.cljs

@@ -169,11 +169,11 @@
   (when-let [page (some-> page-name (db-model/get-page))]
     (when-let [^Object worker @db-browser/*worker]
       (p/let [result (.get-block-and-children worker
-                       (state/get-current-repo)
-                       (str (:block/uuid page))
-                       (ldb/write-transit-str
-                         {:children? true
-                          :nested-children? false}))]
+                                              (state/get-current-repo)
+                                              (str (:block/uuid page))
+                                              (ldb/write-transit-str
+                                               {:children? true
+                                                :nested-children? false}))]
         (some-> result (ldb/read-transit-str) (:children))))))
 
 (defn <get-block-refs
@@ -239,7 +239,7 @@
                                         [(>= ?d ?day)])]
                              date
                              future-day
-                             db-model/block-attrs)]
+                             db-model/file-graph-block-attrs)]
             (->> result
                  db-model/sort-by-order-recursive
                  db-utils/group-by-page)))))))

+ 55 - 26
src/main/frontend/db/model.cljs

@@ -23,7 +23,37 @@
 ;; TODO: extract to specific models and move data transform logic to the
 ;; corresponding handlers.
 
-(def block-attrs ldb/block-attrs)
+(def file-graph-block-attrs
+  "In file graphs, use it to replace '*' for datalog queries"
+  '[:db/id
+    :block/uuid
+    :block/parent
+    :block/order
+    :block/collapsed?
+    :block/format
+    :block/refs
+    :block/_refs
+    :block/path-refs
+    :block/tags
+    :block/link
+    :block/title
+    :block/marker
+    :block/priority
+    :block/properties
+    :block/properties-order
+    :block/properties-text-values
+    :block/pre-block?
+    :block/scheduled
+    :block/deadline
+    :block/repeated?
+    :block/created-at
+    :block/updated-at
+    ;; TODO: remove this in later releases
+    :block/heading-level
+    :block/file
+    :logseq.property/parent
+    {:block/page [:db/id :block/name :block/title :block/journal-day]}
+    {:block/_parent ...}])
 
 (def hidden-page? ldb/hidden?)
 
@@ -213,17 +243,17 @@ independent of format as format specific heading characters are stripped"
 (def sort-by-order ldb/sort-by-order)
 
 (defn sub-block
-  [id & {:keys [ref?]}]
+  [id]
   (when-let [repo (state/get-current-repo)]
     (when id
       (let [ref (react/q repo [:frontend.worker.react/block id]
                          {:query-fn (fn [_]
                                       (let [e (db-utils/entity id)]
                                         [e (:block/tx-id e)]))}
-                         nil)]
-        (if ref?
-          ref
-          (-> ref react first))))))
+                         nil)
+            e (-> ref react first)]
+        (when-let [id (:db/id e)]
+          (db-utils/entity id))))))
 
 (defn sort-by-order-recursive
   [form]
@@ -356,7 +386,7 @@ independent of format as format specific heading characters are stripped"
   [page-name type]
   (let [repo (state/get-current-repo)]
     (when-let [db (conn/get-db repo)]
-     (ldb/page-exists? db page-name type))))
+      (ldb/page-exists? db page-name type))))
 
 (defn page-empty?
   "Whether a page is empty. Does it has a non-page block?
@@ -388,9 +418,9 @@ independent of format as format specific heading characters are stripped"
   [repo block-uuid]
   (when-let [db (conn/get-db repo)]
     (let [ids (ldb/get-block-children-ids db block-uuid)]
-     (when (seq ids)
-       (let [ids' (map (fn [id] [:block/uuid id]) ids)]
-         (db-utils/pull-many repo '[*] ids'))))))
+      (when (seq ids)
+        (let [ids' (map (fn [id] [:block/uuid id]) ids)]
+          (db-utils/pull-many repo '[*] ids'))))))
 
 (defn get-block-and-children
   [repo block-uuid]
@@ -587,7 +617,7 @@ independent of format as format specific heading characters are stripped"
              [?block :block/path-refs ?ref-page]]
            db
            pages
-           (butlast block-attrs))
+           (butlast file-graph-block-attrs))
           (remove (fn [block] (= page-id (:db/id (:block/page block)))))
           db-utils/group-by-page
           (map (fn [[k blocks]]
@@ -720,16 +750,16 @@ independent of format as format specific heading characters are stripped"
 (defn get-all-whiteboards
   [repo]
   (d/q
-    '[:find [(pull ?page [:db/id
-                          :block/uuid
-                          :block/name
-                          :block/title
-                          :block/created-at
-                          :block/updated-at]) ...]
-      :where
-      [?page :block/name]
-      [?page :block/type "whiteboard"]]
-    (conn/get-db repo)))
+   '[:find [(pull ?page [:db/id
+                         :block/uuid
+                         :block/name
+                         :block/title
+                         :block/created-at
+                         :block/updated-at]) ...]
+     :where
+     [?page :block/name]
+     [?page :block/type "whiteboard"]]
+   (conn/get-db repo)))
 
 (defn get-whiteboard-id-nonces
   [repo page-id]
@@ -803,14 +833,14 @@ independent of format as format specific heading characters are stripped"
   (d/q '[:find ?page ?parent
          :where
          [?page :block/namespace ?parent]]
-    (conn/get-db repo)))
+       (conn/get-db repo)))
 
 (defn get-all-namespace-parents
   [repo]
   (let [db (conn/get-db repo)]
     (->> (get-all-namespace-relation repo)
-        (map (fn [[_ ?parent]]
-               (db-utils/entity db ?parent))))))
+         (map (fn [[_ ?parent]]
+                (db-utils/entity db ?parent))))))
 
 ;; Ignore files with empty blocks for now
 (defn get-pages-relation
@@ -885,5 +915,4 @@ independent of format as format specific heading characters are stripped"
        '[:find [(pull ?b [*]) ...]
          :where
          [?b :block/uuid]]
-        (conn/get-db repo))))
-  )
+       (conn/get-db repo)))))

+ 15 - 14
src/main/frontend/db/query_custom.cljs

@@ -12,16 +12,16 @@
 ;; FIXME: what if users want to query other attributes than block-attrs?
 (defn- replace-star-with-block-attrs!
   [l]
-  (let [block-attrs (butlast model/block-attrs)]
+  (let [block-attrs (butlast model/file-graph-block-attrs)]
     (walk/postwalk
-    (fn [f]
-      (if (and (list? f)
-               (= 'pull (first f))
-               (= '?b (second f))
-               (= '[*] (nth f 2)))
-        `(~'pull ~'?b ~block-attrs)
-        f))
-    l)))
+     (fn [f]
+       (if (and (list? f)
+                (= 'pull (first f))
+                (= '?b (second f))
+                (= '[*] (nth f 2)))
+         `(~'pull ~'?b ~block-attrs)
+         f))
+     l)))
 
 (defn- add-rules-to-query
   "Searches query's :where for rules and adds them to query if used"
@@ -60,9 +60,9 @@
                     (fn [rules]
                       (into (or rules [])
                             (rules/extract-rules query-dsl-rules
-                                                   rules-found
-                                                   (when db-graph?
-                                                     {:deps rules/rules-dependencies})))))))
+                                                 rules-found
+                                                 (when db-graph?
+                                                   {:deps rules/rules-dependencies})))))))
       query-m)))
 
 (defn custom-query
@@ -73,12 +73,13 @@
   ([query query-opts]
    (custom-query (state/get-current-repo) query query-opts))
   ([repo query query-opts]
-   (let [query' (replace-star-with-block-attrs! query)
+   (let [db-graph? (config/db-based-graph? repo)
+         query' (if db-graph? query (replace-star-with-block-attrs! query))
          query-opts (if (:query-string query-opts) query-opts
                         (assoc query-opts :query-string (str query)))]
      (if (or (list? (:query query'))
              (not= :find (first (:query query')))) ; dsl query
        (query-dsl/custom-query repo query' query-opts)
        (query-react/react-query repo
-                                (add-rules-to-query query' {:db-graph? (config/db-based-graph? repo)})
+                                (add-rules-to-query query' {:db-graph? db-graph?})
                                 query-opts)))))

+ 1 - 1
src/main/frontend/db/query_dsl.cljs

@@ -606,7 +606,7 @@ Some bindings in this fn:
 
 (defn query-wrapper
   [where {:keys [blocks? block-attrs]}]
-  (let [block-attrs (or block-attrs (butlast model/block-attrs))
+  (let [block-attrs (or block-attrs (butlast model/file-graph-block-attrs))
         q (if blocks?                   ; FIXME: it doesn't need to be either blocks or pages
             `[:find (~'pull ~'?b ~block-attrs)
               :in ~'$ ~'%

+ 62 - 11
src/main/frontend/extensions/code.cljs

@@ -1,5 +1,6 @@
 (ns frontend.extensions.code
-  (:require [clojure.string :as string]
+  (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
             ["codemirror" :as CodeMirror]
             ["codemirror/addon/edit/closebrackets"]
             ["codemirror/addon/edit/matchbrackets"]
@@ -389,15 +390,16 @@
   [config]
   (p/do!
    (code-handler/save-code-editor!)
-   (when-let [block-id (:block/uuid config)]
-     (let [block (db/entity [:block/uuid block-id])]
-       (editor-handler/edit-block! block :max)))))
+   (when-let [block (:block config)]
+     (let [block (db/entity [:block/uuid (:block/uuid block)])]
+       (state/set-state! :editor/raw-mode-block block)
+       (editor-handler/edit-block! block :max {:save-code-editor? false})))))
 
 (defn ^:large-vars/cleanup-todo render!
   [state]
   (let [[config id attr _code theme user-options] (:rum/args state)
+        edit-block (:block config)
         config-file? (= (:file-path config) "logseq/config.edn")
-        edit-block (state/get-edit-block)
         default-open? (and (:editor/code-mode? @state/state)
                            (= (:block/uuid edit-block)
                               (get-in config [:block :block/uuid])))
@@ -443,7 +445,17 @@
             (reset! *editor-ref editor))]
     (when editor
       (let [textarea-ref (rum/ref-node state textarea-ref-name)
-            element (.getWrapperElement editor)]
+            element (.getWrapperElement editor)
+            *cursor-prev (volatile! nil)
+            *cursor-curr (volatile! nil)
+            update-cursor-state! (fn []
+                                   (let [pos (.getCursor editor)
+                                         pos (bean/->clj (js/JSON.parse (js/JSON.stringify pos)))
+                                         pos (select-keys pos [:line :ch])]
+                                     (if (not @*cursor-prev)
+                                       (vreset! *cursor-prev pos)
+                                       (vreset! *cursor-prev @*cursor-curr))
+                                     (vreset! *cursor-curr pos)))]
         (gobj/set textarea-ref codemirror-ref-name editor)
         (when (= mode "calc")
           (.on editor "change" (fn [_cm _e]
@@ -456,18 +468,57 @@
                                     (not (gobj/get cm "escPressed")))
                                (code-handler/save-code-editor!))
                              (state/set-block-component-editing-mode! false)
-                             (state/set-state! :editor/code-block-context nil)))
+                             (state/set-state! :editor/code-block-context nil)
+                             (vreset! *cursor-curr nil)
+                             (vreset! *cursor-prev nil)))
         (.on editor "focus" (fn [_e]
+                              (when (and
+                                     (contains? #{:code} (:logseq.property.node/display-type edit-block))
+                                     (not= (:block/uuid edit-block) (:block/uuid (state/get-edit-block))))
+                                (editor-handler/edit-block! edit-block :max))
+                              (state/set-editing-block-dom-id! (:block-parent-id config))
                               (state/set-block-component-editing-mode! true)
                               (state/set-state! :editor/code-block-context
                                                 {:editor editor
                                                  :config config
                                                  :state state})))
+        (.on editor "cursorActivity" update-cursor-state!)
         (.addEventListener element "keydown" (fn [e]
                                                (let [key-code (.-code e)
                                                      meta-or-ctrl-pressed? (or (.-ctrlKey e) (.-metaKey e))
                                                      shifted? (.-shiftKey e)]
                                                  (cond
+                                                   (contains? #{"ArrowLeft" "ArrowRight"} key-code)
+                                                   (let [direction (if (= "ArrowLeft" key-code) :left :right)
+                                                         line (when-let [line (:line @*cursor-curr)]
+                                                                (.getLine (.-doc editor) line))]
+                                                     (when (and (= @*cursor-prev @*cursor-curr)
+                                                                (or (and direction (nil? @*cursor-curr))
+                                                                    (case direction
+                                                                      :left (and (zero? (:line @*cursor-curr))
+                                                                                 (zero? (:ch  @*cursor-curr)))
+                                                                      :right (and (= (:line @*cursor-curr) (.lastLine editor))
+                                                                                  (= (count line) (:ch @*cursor-curr)))
+                                                                      false)))
+                                                       (editor-handler/move-to-block-when-cross-boundary direction {}))
+                                                     (update-cursor-state!))
+
+                                                   (contains? #{"ArrowUp" "ArrowDown"} key-code)
+                                                   (let [direction (if (= "ArrowUp" key-code) :up :down)
+                                                         line (when-let [line (:line @*cursor-curr)]
+                                                                (.getLine (.-doc editor) line))]
+                                                     (when (and (= @*cursor-prev @*cursor-curr)
+                                                                (or (and direction (nil? @*cursor-curr))
+                                                                    (case direction
+                                                                      :up (and (zero? (:line @*cursor-curr))
+                                                                               (zero? (:ch  @*cursor-curr)))
+                                                                      :down (and (= (:line @*cursor-curr) (.lastLine editor))
+                                                                                 (= (count line) (:ch @*cursor-curr)))
+                                                                      false)))
+                                                       (editor-handler/move-cross-boundary-up-down
+                                                        direction {:input textarea
+                                                                   :pos [direction 0]}))
+                                                     (update-cursor-state!))
                                                    meta-or-ctrl-pressed?
                                                    ;; prevent default behavior of browser
                                                    ;; Cmd + [ => Go back in browser, outdent in CodeMirror
@@ -478,15 +529,15 @@
                                                      nil)
                                                    shifted?
                                                    (case key-code
+                                                     ;; create new block
                                                      "Enter"
                                                      (do
                                                        (util/stop e)
                                                        (when-let [blockid (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid"))]
                                                          (code-handler/save-code-editor!)
-                                                         (js/setTimeout
-                                                          #(editor-handler/api-insert-new-block! ""
-                                                                                                 {:block-uuid (uuid blockid)
-                                                                                                  :sibling? true}) 32)))
+                                                         (util/schedule #(editor-handler/api-insert-new-block! ""
+                                                                                                               {:block-uuid (uuid blockid)
+                                                                                                                :sibling? true}))))
                                                      nil)))))
         (.addEventListener element "pointerdown"
                            (fn [e]

+ 1 - 1
src/main/frontend/extensions/fsrs.cljs

@@ -81,7 +81,7 @@
   [repo cards-id]
   (let [now-inst-ms (inst-ms (js/Date.))
         cards (when (and cards-id (not= (keyword cards-id) :global)) (db/entity cards-id))
-        query (:block/title (:logseq.property/query cards))
+        query (:block/title cards)
         result (query-dsl/parse query {:db-graph? true})
         q '[:find [?b ...]
             :in $ ?now-inst-ms %

+ 20 - 15
src/main/frontend/format/block.cljs

@@ -29,13 +29,13 @@ and handles unexpected failure."
                                              :db-graph-mode? (config/db-based-graph? repo)})]
         (if (config/db-based-graph? repo)
           (map (fn [block]
-                (cond-> (dissoc block :block/properties :block/macros :block/properties-order)
-                  (:block/properties block)
-                  (merge (update-keys (:block/properties block)
-                                      (fn [k]
-                                        (or ({:heading :logseq.property/heading} k)
-                                            (throw (ex-info (str "Don't know how to save graph-parser property " (pr-str k)) {}))))))))
-              blocks)
+                 (cond-> (dissoc block :block/properties :block/macros :block/properties-order)
+                   (:block/properties block)
+                   (merge (update-keys (:block/properties block)
+                                       (fn [k]
+                                         (or ({:heading :logseq.property/heading} k)
+                                             (throw (ex-info (str "Don't know how to save graph-parser property " (pr-str k)) {}))))))))
+               blocks)
           blocks))
       (catch :default e
         (log/error :exception e)
@@ -78,7 +78,12 @@ and handles unexpected failure."
           format (or format :markdown)
           parse-config (mldoc/get-default-config format)
           ast (format/to-edn title format parse-config)
-          blocks (extract-blocks ast title format {:parse-block block})
+          ;; Disable extraction for display-type blocks as there isn't a reason to have
+          ;; it enabled yet and can cause visible bugs when '#' is used
+          blocks (if (and (config/db-based-graph? (state/get-current-repo))
+                          (:logseq.property.node/display-type block))
+                   [block]
+                   (extract-blocks ast title format {:parse-block block}))
           new-block (first blocks)
           block (cond->
                  (merge block new-block)
@@ -126,13 +131,13 @@ and handles unexpected failure."
     (if (= typ "Paragraph")
       (let [indexed-paras (map-indexed vector paras)]
         [typ (->> (filter
-                            #(let [[index value] %]
-                               (not (and (> index 0)
-                                         (= value ["Break_Line"])
-                                         (contains? #{"Timestamp" "Macro"}
-                                                    (first (nth paras (dec index)))))))
-                            indexed-paras)
-                           (map #(last %)))])
+                   #(let [[index value] %]
+                      (not (and (> index 0)
+                                (= value ["Break_Line"])
+                                (contains? #{"Timestamp" "Macro"}
+                                           (first (nth paras (dec index)))))))
+                   indexed-paras)
+                  (map #(last %)))])
       ast)))
 
 (defn trim-break-lines!

+ 9 - 6
src/main/frontend/handler/block.cljs

@@ -171,12 +171,14 @@
     (state/set-editor-last-input-time! repo (util/time-ms))))
 
 (defn- edit-block-aux
-  [repo block content text-range {:keys [container-id]}]
+  [repo block content text-range {:keys [container-id direction event pos]}]
   (when block
     (let [container-id (or container-id
                            (state/get-current-editor-container-id)
                            :unknown-container)]
-      (state/set-editing! (str "edit-block-" (:block/uuid block)) content block text-range {:container-id container-id}))
+      (state/set-editing! (str "edit-block-" (:block/uuid block)) content block text-range
+                          {:db (db/get-db)
+                           :container-id container-id :direction direction :event event :pos pos}))
     (mark-last-input-time! repo)))
 
 (defn sanity-block-content
@@ -187,12 +189,13 @@
         (drawer/remove-logbook))))
 
 (defn edit-block!
-  [block pos & {:keys [_container-id custom-content tail-len]
-                :or {tail-len 0}
+  [block pos & {:keys [_container-id custom-content tail-len save-code-editor?]
+                :or {tail-len 0
+                     save-code-editor? true}
                 :as opts}]
   (when (and (not config/publishing?) (:block/uuid block))
     (p/do!
-     (state/pub-event! [:editor/save-code-editor])
+     (when save-code-editor? (state/pub-event! [:editor/save-code-editor]))
      (when (not= (:block/uuid block) (:block/uuid (state/get-edit-block)))
        (state/clear-edit! {:clear-editing-block? false}))
      (when-let [block-id (:block/uuid block)]
@@ -214,7 +217,7 @@
                           (subs content 0 pos))
              content (sanity-block-content repo (:block/format block) content)]
          (state/clear-selection!)
-         (edit-block-aux repo block content text-range opts))))))
+         (edit-block-aux repo block content text-range (assoc opts :pos pos)))))))
 
 (defn- get-original-block-by-dom
   [node]

+ 1 - 1
src/main/frontend/handler/code.cljs

@@ -27,7 +27,7 @@
           ;; update default value for the editor initial state
           (set! ds -v value)
           (cond
-            (= :code (:logseq.property.node/type block))
+            (= :code (:logseq.property.node/display-type block))
             (editor-handler/save-block-if-changed! block value)
 
             ;; save block content

+ 11 - 6
src/main/frontend/handler/db_based/page.cljs

@@ -48,11 +48,11 @@
   (if (db/page-exists? (:block/title page-entity) "class")
     (notification/show! (str "A tag with the name \"" (:block/title page-entity) "\" already exists.") :warning false)
     (let [class (db-class/build-new-class (db/get-db)
-                                         {:db/id (:db/id page-entity)
-                                          :block/title (:block/title page-entity)
-                                          :block/created-at (:block/created-at page-entity)})]
+                                          {:db/id (:db/id page-entity)
+                                           :block/title (:block/title page-entity)
+                                           :block/created-at (:block/created-at page-entity)})]
 
-     (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block}))))
+      (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block}))))
 
 (defn <create-class!
   "Creates a class page and provides class-specific error handling"
@@ -82,6 +82,11 @@
                 add-tag-to-nearest-node? (= page-ref/right-brackets (common-util/safe-subs edit-content (- hash-idx 2) hash-idx))
                 nearest-node (some-> (editor-handler/get-nearest-page) string/trim)]
             (if (and add-tag-to-nearest-node? (not (string/blank? nearest-node)))
-              (when-let [e (db/get-case-page nearest-node)]
-                (add-tag (state/get-current-repo) (:block/uuid e) tag-entity))
+              (p/let [node-ent (db/get-case-page nearest-node)
+                      ;; Save because nearest node doesn't exist yet
+                      _ (when-not node-ent (editor-handler/save-current-block!))
+                      node-ent' (or node-ent (db/get-case-page nearest-node))
+                      _ (add-tag (state/get-current-repo) (:block/uuid node-ent') tag-entity)]
+                ;; Notify as action has been applied to a node off screen
+                (notification/show! (str "Added tag " (pr-str (:block/title tag-entity)) " to " (pr-str (:block/title node-ent')))))
               (add-tag (state/get-current-repo) (:block/uuid edit-block) tag-entity))))))))

+ 60 - 74
src/main/frontend/handler/editor.cljs

@@ -29,7 +29,6 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.file :as property-file]
             [frontend.handler.property.util :as pu]
-            [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.op :as outliner-op]
@@ -39,7 +38,6 @@
             [frontend.state :as state]
             [frontend.template :as template]
             [frontend.util :as util]
-            [frontend.util.file-based.clock :as clock]
             [frontend.util.cursor :as cursor]
             [frontend.util.file-based.drawer :as drawer]
             [frontend.util.keycode :as keycode]
@@ -610,43 +608,11 @@
                {:outliner-op :insert-blocks}
                (outliner-op/insert-blocks! [new-block] page {:sibling? false})))))))))
 
-(defn update-timestamps-content!
-  [{:block/keys [repeated? marker format] :as block} content]
-  (if repeated?
-    (let [scheduled-ast (block-handler/get-scheduled-ast block)
-          deadline-ast (block-handler/get-deadline-ast block)
-          content (some->> (filter repeated/repeated? [scheduled-ast deadline-ast])
-                           (map (fn [ts]
-                                  [(repeated/timestamp->text ts)
-                                   (repeated/next-timestamp-text ts)]))
-                           (reduce (fn [content [old new]]
-                                     (string/replace content old new))
-                                   content))
-          content (string/replace-first
-                   content marker
-                   (case marker
-                     "DOING"
-                     "TODO"
-
-                     "NOW"
-                     "LATER"
-
-                     marker))
-          content (clock/clock-out format content)
-          content (drawer/insert-drawer
-                   format content "logbook"
-                   (util/format (str (if (= :org format) "-" "*")
-                                     " State \"DONE\" from \"%s\" [%s]")
-                                marker
-                                (date/get-date-time-string-3)))]
-      content)
-    content))
-
 (defn check
   [{:block/keys [marker title repeated? uuid] :as block}]
   (let [new-content (string/replace-first title marker "DONE")
         new-content (if repeated?
-                      (update-timestamps-content! block title)
+                      (file-editor-handler/update-timestamps-content! block title)
                       new-content)
         input-id (state/get-edit-input-id)]
     (if (and input-id
@@ -845,7 +811,7 @@
                           db-based? (config/db-based-graph? repo)
                           delete-prev-block? (and db-based?
                                                   (empty? (:block/tags block))
-                                                  (not (:logseq.property.node/type block))
+                                                  (not (:logseq.property.node/display-type block))
                                                   (seq (:block/properties block))
                                                   (empty? (:block/properties prev-block))
                                                   (not (:logseq.property/created-from-property block)))]
@@ -940,6 +906,11 @@
     (set-blocks-id! [block-id])
     (util/copy-to-clipboard! (tap-clipboard block-id)))))
 
+(defn copy-block-content!
+  [block]
+  (util/copy-to-clipboard! (:block/title block))
+  (notification/show! "Copied!" :success))
+
 (defn select-block!
   [block-uuid]
   (block-handler/select-block! block-uuid))
@@ -2651,11 +2622,10 @@
       (util/scroll-to-block sibling-block)
       (state/exit-editing-and-set-selected-blocks! [sibling-block]))))
 
-(defn- move-cross-boundary-up-down
+(defn move-cross-boundary-up-down
   [direction move-opts]
-  (when-let [input (state/get-input)]
-    (let [line-pos (util/get-line-pos (.-value input) (util/get-selection-start input))
-          repo (state/get-current-repo)
+  (when-let [input (or (:input move-opts) (state/get-input))]
+    (let [repo (state/get-current-repo)
           f (case direction
               :up util/get-prev-block-non-collapsed
               :down util/get-next-block-non-collapsed)
@@ -2667,15 +2637,17 @@
           (let [container-id (some-> (dom/attr sibling-block "containerid") js/parseInt)
                 value (state/get-edit-content)]
             (p/do!
-             (when (not= (clean-content! repo format title)
-                         (string/trim value))
+             (when (and
+                    (not (state/block-component-editing?))
+                    (not= (clean-content! repo format title)
+                          (string/trim value)))
                (save-block! repo uuid value))
 
              (let [new-uuid (cljs.core/uuid sibling-block-id)
                    block (db/entity [:block/uuid new-uuid])]
                (edit-block! block
                             (or (:pos move-opts)
-                                [direction line-pos])
+                                [direction (util/get-line-pos (.-value input) (util/get-selection-start input))])
                             {:container-id container-id
                              :direction direction})))))
         (case direction
@@ -2704,11 +2676,11 @@
         (cursor/move-cursor-up input)
         (cursor/move-cursor-down input)))))
 
-(defn- move-to-block-when-cross-boundary
-  [direction]
+(defn move-to-block-when-cross-boundary
+  [direction {:keys [block]}]
   (let [up? (= :left direction)
         pos (if up? :max 0)
-        {:block/keys [format uuid] :as block} (state/get-edit-block)
+        {:block/keys [format uuid] :as block} (or block (state/get-edit-block))
         repo (state/get-current-repo)
         editing-block (gdom/getElement (state/get-editing-block-dom-id))
         f (if up? util/get-prev-block-non-collapsed util/get-next-block-non-collapsed)
@@ -2754,7 +2726,7 @@
 
         (or (and left? (cursor/start? input))
             (and right? (cursor/end? input)))
-        (move-to-block-when-cross-boundary direction)
+        (move-to-block-when-cross-boundary direction {})
 
         :else
         (if left?
@@ -3380,7 +3352,8 @@
       (util/stop e)
       (let [block {:block/uuid block-id}
             left? (= direction :left)
-            opts {:container-id (some-> node (dom/attr "containerid") (parse-long))}]
+            opts {:container-id (some-> node (dom/attr "containerid") (parse-long))
+                  :event e}]
         (edit-block! block (if left? 0 :max) opts)))))
 
 (defn shortcut-left-right [direction]
@@ -3446,17 +3419,19 @@
            (mldoc/block-with-title? first-elem-type))
          true)))
 
-(defn- valid-custom-query-block?
-  "Whether block has a valid custom query."
+(defn- db-collapsable?
   [block]
-  (let [entity (db/entity (:db/id block))
-        content (:block/title entity)]
-    (when content
-      (when (and (string/includes? content "#+BEGIN_QUERY")
-                 (string/includes? content "#+END_QUERY"))
-        (let [ast (mldoc/->edn (string/trim content) (or (:block/format entity) :markdown))
-              q (mldoc/extract-first-query-from-ast ast)]
-          (some? (:query (common-util/safe-read-map-string q))))))))
+  (let [property-keys (->> (keys (:block/properties block))
+                           (remove db-property/db-attribute-properties)
+                           (remove #(outliner-property/property-with-other-position? (db/entity %))))]
+    (or (ldb/class-instance? (db/entity :logseq.class/Query) block)
+        (and (seq property-keys)
+             (not (db-pu/all-hidden-properties? property-keys)))
+        (and (seq (:block/tags block))
+             (some (fn [t]
+                     (let [properties (map :db/ident (:logseq.property.class/properties t))]
+                       (and (seq properties)
+                            (not (db-pu/all-hidden-properties? properties))))) (:block/tags block))))))
 
 (defn collapsable?
   ([block-id]
@@ -3467,23 +3442,12 @@
    (when block-id
      (let [repo (state/get-current-repo)]
        (if-let [block (db/entity [:block/uuid block-id])]
-         (let [db-based? (config/db-based-graph? repo)
-               tags (:block/tags (db/entity (:db/id block)))
-               property-keys (->> (keys (:block/properties block))
-                                  (remove db-property/db-attribute-properties)
-                                  (remove #(outliner-property/property-with-other-position? (db/entity %))))]
+         (let [db-based? (config/db-based-graph? repo)]
            (or (if ignore-children? false (db-model/has-children? block-id))
-               (and (not db-based?) (file-editor-handler/valid-dsl-query-block? block))
-               (valid-custom-query-block? block)
-               (and db-based? (ldb/class-instance? (db/entity :logseq.class/Query) block))
-               (and db-based?
-                    (seq property-keys)
-                    (not (db-pu/all-hidden-properties? property-keys)))
-               (and db-based? (seq tags)
-                    (some (fn [t]
-                            (let [properties (map :db/ident (:logseq.property.class/properties (:block/schema t)))]
-                              (and (seq properties)
-                                   (not (db-pu/all-hidden-properties? properties))))) tags))
+               (and db-based? (db-collapsable? block))
+               (and (not db-based?)
+                    (or (file-editor-handler/valid-dsl-query-block? block)
+                        (file-editor-handler/valid-custom-query-block? block)))
                (and
                 (:outliner/block-title-collapse-enabled? (state/get-config))
                 (block-with-title? (:block/format block)
@@ -3934,3 +3898,25 @@
   (.setData (gobj/get event "dataTransfer")
             (if page? "page-name" "block-uuid")
             (str block-or-page-name)))
+
+(defn run-query-command!
+  []
+  (let [repo (state/get-current-repo)]
+    (when-let [block (some-> (state/get-edit-block)
+                             :db/id
+                             (db/entity))]
+      (p/do!
+       (save-current-block!)
+       (state/clear-edit!)
+       (p/let [query-block (or (:logseq.property/query block)
+                               (p/do!
+                                (property-handler/set-block-property! repo (:db/id block) :logseq.property/query "")
+                                (:logseq.property/query (db/entity (:db/id block)))))
+               current-query (:block/title (db/entity (:db/id block)))]
+         (p/do!
+          (ui-outliner-tx/transact!
+           {:outliner-op :save-block}
+           (property-handler/set-block-property! repo (:db/id block) :block/tags :logseq.class/Query)
+           (save-block-inner! block "" {})
+           (when query-block
+             (save-block-inner! query-block current-query {})))))))))

+ 53 - 1
src/main/frontend/handler/events.cljs

@@ -55,6 +55,7 @@
             [frontend.handler.code :as code-handler]
             [frontend.handler.db-based.rtc :as rtc-handler]
             [frontend.handler.graph :as graph-handler]
+            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.mobile.core :as mobile]
             [frontend.mobile.graph-picker :as graph-picker]
             [frontend.mobile.util :as mobile-util]
@@ -80,7 +81,9 @@
             [logseq.db :as ldb]
             [frontend.persist-db :as persist-db]
             [frontend.handler.export :as export]
-            [frontend.extensions.fsrs :as fsrs]))
+            [frontend.extensions.fsrs :as fsrs]
+            [frontend.storage :as storage]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]))
 
 ;; TODO: should we move all events here?
 
@@ -885,6 +888,22 @@
 (defmethod handle :editor/save-code-editor [_]
   (code-handler/save-code-editor!))
 
+(defmethod handle :editor/focus-code-editor [[_ editing-block container]]
+  (when-let [^js cm (util/get-cm-instance container)]
+    (when-not (.hasFocus cm)
+      (let [cursor-pos (some-> (:editor/cursor-range @state/state) (deref) (count))
+            direction (:block.editing/direction editing-block)
+            pos (:block.editing/pos editing-block)
+            to-line (case direction
+                      :up (.lastLine cm)
+                      (case pos
+                        :max (.lastLine cm)
+                        0))]
+                 ;; move to friendly cursor
+        (doto cm
+          (.focus)
+          (.setCursor to-line (or cursor-pos 0)))))))
+
 (defmethod handle :editor/toggle-children-number-list [[_ block]]
   (when-let [blocks (and block (db-model/get-block-immediate-children (state/get-current-repo) (:block/uuid block)))]
     (editor-handler/toggle-blocks-as-own-order-list! blocks)))
@@ -945,6 +964,36 @@
                               {:id :property-dialog
                                :align "start"})))))))
 
+(defmethod handle :editor/upsert-type-block [[_ {:keys [block type lang]}]]
+  (p/do!
+   (editor-handler/save-current-block!)
+   (p/delay 16)
+   (let [block (db/entity (:db/id block))
+         block-type (:logseq.property.node/display-type block)
+         block-title (:block/title block)
+         latest-code-lang (or lang (storage/get :latest-code-lang))
+         turn-type! #(if (and (= (keyword type) :code) latest-code-lang)
+                       (db-property-handler/set-block-properties!
+                        (:block/uuid %)
+                        {:logseq.property.node/display-type (keyword type)
+                         :logseq.property.code/lang latest-code-lang})
+                       (db-property-handler/set-block-property!
+                        (:block/uuid %) :logseq.property.node/display-type (keyword type)))]
+     (p/let [block (if (or (not (nil? block-type))
+                           (not (string/blank? block-title)))
+                     (p/let [result (ui-outliner-tx/transact!
+                                     {:outliner-op :insert-blocks}
+                                     ;; insert a new block
+                                     (let [[_p _ block'] (editor-handler/insert-new-block-aux! {} block "")]
+                                       (turn-type! block')))
+                             result' (ldb/read-transit-str result)]
+                       (when-let [id (:block/uuid (first (:blocks result')))]
+                         (db/entity [:block/uuid id])))
+                     (p/do!
+                      (turn-type! block)
+                      (db/entity [:block/uuid (:block/uuid block)])))]
+       (js/setTimeout #(editor-handler/edit-block! block :max) 100)))))
+
 (rum/defc multi-tabs-dialog
   []
   (let [word (if (util/electron?) "window" "tab")]
@@ -987,6 +1036,9 @@
 
     nil))
 
+(defmethod handle :editor/run-query-command [_]
+  (editor-handler/run-query-command!))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 48 - 0
src/main/frontend/handler/file_based/editor.cljs

@@ -3,6 +3,7 @@
   (:require [clojure.string :as string]
             [frontend.config :as config]
             [frontend.commands :as commands]
+            [frontend.date :as date]
             [frontend.format.block :as block]
             [frontend.db :as db]
             [frontend.db.query-dsl :as query-dsl]
@@ -13,12 +14,15 @@
             [frontend.util :as util]
             [frontend.util.file-based.clock :as clock]
             [frontend.util.file-based.drawer :as drawer]
+            [frontend.handler.file-based.repeated :as repeated]
+            [frontend.handler.block :as block-handler]
             [frontend.handler.file-based.status :as status]
             [frontend.handler.property.file :as property-file]
             [frontend.handler.file-based.property :as file-property-handler]
             [frontend.handler.file-based.property.util :as property-util]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.common.util.block-ref :as block-ref]
+            [logseq.common.util :as common-util]
             [logseq.db :as ldb]))
 
 (defn- remove-non-existed-refs!
@@ -227,3 +231,47 @@
                            (query-dsl/parse-query query-body)
                            (catch :default _e
                              nil))))))))))
+
+(defn valid-custom-query-block?
+  "Whether block has a valid custom query."
+  [block]
+  (let [entity (db/entity (:db/id block))
+        content (:block/title entity)]
+    (when content
+      (when (and (string/includes? content "#+BEGIN_QUERY")
+                 (string/includes? content "#+END_QUERY"))
+        (let [ast (mldoc/->edn (string/trim content) (or (:block/format entity) :markdown))
+              q (mldoc/extract-first-query-from-ast ast)]
+          (some? (:query (common-util/safe-read-map-string q))))))))
+
+(defn update-timestamps-content!
+  [{:block/keys [repeated? marker format] :as block} content]
+  (if repeated?
+    (let [scheduled-ast (block-handler/get-scheduled-ast block)
+          deadline-ast (block-handler/get-deadline-ast block)
+          content (some->> (filter repeated/repeated? [scheduled-ast deadline-ast])
+                           (map (fn [ts]
+                                  [(repeated/timestamp->text ts)
+                                   (repeated/next-timestamp-text ts)]))
+                           (reduce (fn [content [old new]]
+                                     (string/replace content old new))
+                                   content))
+          content (string/replace-first
+                   content marker
+                   (case marker
+                     "DOING"
+                     "TODO"
+
+                     "NOW"
+                     "LATER"
+
+                     marker))
+          content (clock/clock-out format content)
+          content (drawer/insert-drawer
+                   format content "logbook"
+                   (util/format (str (if (= :org format) "-" "*")
+                                     " State \"DONE\" from \"%s\" [%s]")
+                                marker
+                                (date/get-date-time-string-3)))]
+      content)
+    content))

+ 1 - 1
src/main/frontend/handler/repeated.cljs → src/main/frontend/handler/file_based/repeated.cljs

@@ -1,4 +1,4 @@
-(ns frontend.handler.repeated
+(ns frontend.handler.file-based.repeated
   "Provides fns related to schedule and deadline"
   (:require [cljs-time.core :as t]
             [cljs-time.local :as tl]

+ 100 - 96
src/main/frontend/state.cljs

@@ -164,6 +164,7 @@
       :editor/cursor-range                   (atom nil)
       :editor/container-id                   (atom nil)
       :editor/next-edit-block                (atom nil)
+      :editor/raw-mode-block                 (atom nil)
 
       :selection/mode                        (atom false)
       ;; Warning: blocks order is determined when setting this attribute
@@ -384,8 +385,8 @@
                       [(<= ?d ?today)]]
              :inputs [:14d :today]
              :result-transform '(fn [result]
-                                 (sort-by (fn [h]
-                                            (get h :block/priority "Z")) result))
+                                  (sort-by (fn [h]
+                                             (get h :block/priority "Z")) result))
              :group-by-page? false
              :collapsed? false}
             {:title "📅 NEXT"
@@ -443,10 +444,10 @@
   (->> configs
        (filter map?)
        (apply merge-with
-         (fn merge-config [current new]
-           (if (and (map? current) (map? new))
-             (merge current new)
-             new)))))
+              (fn merge-config [current new]
+                (if (and (map? current) (map? new))
+                  (merge current new)
+                  new)))))
 
 (defn get-global-config
   []
@@ -482,13 +483,13 @@ should be done through this fn in order to get global config and config defaults
   (or (not @publishing?) (:publishing/enable-editing? (get-config))))
 
 (defonce built-in-macros
-         {"img" "[:img.$4 {:src \"$1\" :style {:width $2 :height $3}}]"})
+  {"img" "[:img.$4 {:src \"$1\" :style {:width $2 :height $3}}]"})
 
 (defn get-macros
   []
   (merge
-    built-in-macros
-    (:macros (get-config))))
+   built-in-macros
+   (:macros (get-config))))
 
 (defn set-assets-alias-enabled!
   [v]
@@ -534,9 +535,9 @@ should be done through this fn in order to get global config and config defaults
    (get-preferred-format (get-current-repo)))
   ([repo-url]
    (keyword
-     (or
-      (common-config/get-preferred-format (get-config repo-url))
-      (get-in @state [:me :preferred_format] "markdown")))))
+    (or
+     (common-config/get-preferred-format (get-config repo-url))
+     (get-in @state [:me :preferred_format] "markdown")))))
 
 (defn markdown?
   []
@@ -546,16 +547,16 @@ should be done through this fn in order to get global config and config defaults
 (defn get-pages-directory
   []
   (or
-    (when-let [repo (get-current-repo)]
-      (:pages-directory (get-config repo)))
-    "pages"))
+   (when-let [repo (get-current-repo)]
+     (:pages-directory (get-config repo)))
+   "pages"))
 
 (defn get-journals-directory
   []
   (or
-    (when-let [repo (get-current-repo)]
-      (:journals-directory (get-config repo)))
-    "journals"))
+   (when-let [repo (get-current-repo)]
+     (:journals-directory (get-config repo)))
+   "journals"))
 
 (defn get-whiteboards-directory
   []
@@ -576,13 +577,13 @@ should be done through this fn in order to get global config and config defaults
 (defn get-preferred-workflow
   []
   (keyword
-    (or
-      (when-let [workflow (:preferred-workflow (get-config))]
-        (let [workflow (name workflow)]
-          (if (util/safe-re-find #"now|NOW" workflow)
-            :now
-            :todo)))
-      (get-in @state [:me :preferred_workflow] :now))))
+   (or
+    (when-let [workflow (:preferred-workflow (get-config))]
+      (let [workflow (name workflow)]
+        (if (util/safe-re-find #"now|NOW" workflow)
+          :now
+          :todo)))
+    (get-in @state [:me :preferred_workflow] :now))))
 
 (defn get-preferred-todo
   []
@@ -601,8 +602,8 @@ should be done through this fn in order to get global config and config defaults
     (if (sqlite-util/db-based-graph? repo)
       (when-let [conn (db-conn-state/get-conn repo)]
         (get (d/entity @conn :logseq.class/Journal)
-           :logseq.property.journal/title-format
-           "MMM do, yyyy"))
+             :logseq.property.journal/title-format
+             "MMM do, yyyy"))
       (common-config/get-date-formatter (get-config)))))
 
 (defn shortcuts []
@@ -626,18 +627,18 @@ should be done through this fn in order to get global config and config defaults
 (defn get-ref-open-blocks-level
   []
   (or
-    (when-let [value (:ref/default-open-blocks-level (get-config))]
-      (when (integer? value)
-        value))
-    2))
+   (when-let [value (:ref/default-open-blocks-level (get-config))]
+     (when (integer? value)
+       value))
+   2))
 
 (defn get-linked-references-collapsed-threshold
   []
   (or
-    (when-let [value (:ref/linked-references-collapsed-threshold (get-config))]
-      (when (integer? value)
-        value))
-    100))
+   (when-let [value (:ref/linked-references-collapsed-threshold (get-config))]
+     (when (integer? value)
+       value))
+   100))
 
 (defn get-export-bullet-indentation
   []
@@ -1463,12 +1464,12 @@ Similar to re-frame subscriptions"
   []
   (when (util/electron?)
     (js/window.apis.setUpdatesCallback
-      (fn [_ args]
-        (let [data (bean/->clj args)
-              pending? (not= (:type data) "completed")]
-          (set-state! :electron/updater-pending? pending?)
-          (when pending? (set-state! :electron/updater data))
-          nil)))))
+     (fn [_ args]
+       (let [data (bean/->clj args)
+             pending? (not= (:type data) "completed")]
+         (set-state! :electron/updater-pending? pending?)
+         (when pending? (set-state! :electron/updater data))
+         nil)))))
 
 (defn set-file-component!
   [component]
@@ -1538,17 +1539,17 @@ Similar to re-frame subscriptions"
            idx (and id (first (keep-indexed #(when (= (:modal/id %2) id) %1)
                                             modals)))
            input (medley/filter-vals
-                   #(not (nil? %1))
-                   {:modal/id            id
-                    :modal/label         (if label (name label) "")
-                    :modal/class         (if center? "as-center" "")
-                    :modal/payload       payload
-                    :modal/show?         (if (boolean? show?) show? true)
-                    :modal/panel-content panel-content
-                    :modal/close-btn?    close-btn?
-                    :modal/close-backdrop? (if (boolean? close-backdrop?) close-backdrop? true)})]
+                  #(not (nil? %1))
+                  {:modal/id            id
+                   :modal/label         (if label (name label) "")
+                   :modal/class         (if center? "as-center" "")
+                   :modal/payload       payload
+                   :modal/show?         (if (boolean? show?) show? true)
+                   :modal/panel-content panel-content
+                   :modal/close-btn?    close-btn?
+                   :modal/close-backdrop? (if (boolean? close-backdrop?) close-backdrop? true)})]
        (swap! state update-in
-         [:modal/subsets (or idx (count modals))]
+              [:modal/subsets (or idx (count modals))]
               merge input)
        (:modal/subsets @state)))))
 
@@ -1563,7 +1564,7 @@ Similar to re-frame subscriptions"
        (if (and id (not (string/blank? mid)) (= id mid))
          (close-modal!)
          (when-let [idx (if id (first (keep-indexed #(when (= (:modal/id %2) id) %1) modals))
-                          (dec (count modals)))]
+                            (dec (count modals)))]
            (swap! state assoc :modal/subsets (into [] (medley/remove-nth idx modals)))))))
    (:modal/subsets @state)))
 
@@ -1639,7 +1640,7 @@ Similar to re-frame subscriptions"
 (defn toggle-left-sidebar!
   []
   (set-left-sidebar-open!
-    (not (get-left-sidebar-open?))))
+   (not (get-left-sidebar-open?))))
 
 (defn set-developer-mode!
   [value]
@@ -1726,7 +1727,7 @@ Similar to re-frame subscriptions"
   (when-let [resource (get-plugin-resource pid type key)]
     (let [resource (assoc resource (keyword attr) val)]
       (set-state!
-        [:plugin/installed-resources (keyword pid) (keyword type) key] resource)
+       [:plugin/installed-resources (keyword pid) (keyword type) key] resource)
       resource)))
 
 (defn get-plugin-services
@@ -1791,8 +1792,8 @@ Similar to re-frame subscriptions"
      (set-state!
       [:plugin/installed-hooks hook]
       (assoc
-        ((fnil identity {}) (get-in @state [:plugin/installed-hooks hook]))
-        pid opts)) true)))
+       ((fnil identity {}) (get-in @state [:plugin/installed-hooks hook]))
+       pid opts)) true)))
 
 (defn uninstall-plugin-hook
   [pid hook-or-all]
@@ -2033,7 +2034,7 @@ Similar to re-frame subscriptions"
    (set-selection-blocks! blocks direction)))
 
 (defn set-editing!
-  [edit-input-id content block cursor-range & {:keys [move-cursor? container-id property-block]
+  [edit-input-id content block cursor-range & {:keys [db move-cursor? container-id property-block direction event pos]
                                                :or {move-cursor? true}}]
   (when-not (exists? js/process)
     (if (> (count content)
@@ -2043,39 +2044,43 @@ Similar to re-frame subscriptions"
           (util/scroll-to-element (gobj/get (first elements) "id")))
         (exit-editing-and-set-selected-blocks! elements))
       (when (and edit-input-id block
-                   (or
-                    (publishing-enable-editing?)
-                    (not @publishing?)))
-          (let [block-element (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block"))
-                container (util/get-block-container block-element)
-                block (if container
-                        (assoc block
-                               :block.temp/container (gobj/get container "id"))
-                        block)
-                content (string/trim (or content ""))]
-            (assert (and container-id (:block/uuid block))
-                    "container-id or block uuid is missing")
-            (set-state! :editor/block-refs #{})
-            (if property-block
-              (set-editing-block-id! [container-id (:block/uuid property-block) (:block/uuid block)])
-              (set-editing-block-id! [container-id (:block/uuid block)]))
-            (set-state! :editor/container-id container-id)
-            (set-state! :editor/block block)
-            (set-state! :editor/content content :path-in-sub-atom (:block/uuid block))
-            (set-state! :editor/last-key-code nil)
-            (set-state! :editor/set-timestamp-block nil)
-            (set-state! :editor/cursor-range cursor-range)
-
-            (when-let [input (gdom/getElement edit-input-id)]
-              (let [pos (count cursor-range)]
-                (when content
-                  (util/set-change-value input content))
-
-                (when move-cursor?
-                  (cursor/move-cursor-to input pos))
-
-                (when (or (util/mobile?) (mobile-util/native-platform?))
-                  (set-state! :mobile/show-action-bar? false)))))))))
+                 (or
+                  (publishing-enable-editing?)
+                  (not @publishing?)))
+        (let [block-element (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block"))
+              container (util/get-block-container block-element)
+              block (if container
+                      (assoc block
+                             :block.temp/container (gobj/get container "id"))
+                      block)
+              block (assoc block :block.editing/direction direction
+                           :block.editing/event event
+                           :block.editing/pos pos)
+              content (string/trim (or content ""))]
+          (assert (and container-id (:block/uuid block))
+                  "container-id or block uuid is missing")
+          (set-state! :editor/block-refs #{})
+          (if property-block
+            (set-editing-block-id! [container-id (:block/uuid property-block) (:block/uuid block)])
+            (set-editing-block-id! [container-id (:block/uuid block)]))
+          (set-state! :editor/container-id container-id)
+          (set-state! :editor/block block)
+          (set-state! :editor/content content :path-in-sub-atom (:block/uuid block))
+          (set-state! :editor/last-key-code nil)
+          (set-state! :editor/set-timestamp-block nil)
+          (set-state! :editor/cursor-range cursor-range)
+          (when (= :code (:logseq.property.node/display-type (d/entity db (:db/id block))))
+            (pub-event! [:editor/focus-code-editor block block-element]))
+          (when-let [input (gdom/getElement edit-input-id)]
+            (let [pos (count cursor-range)]
+              (when content
+                (util/set-change-value input content))
+
+              (when (and move-cursor? (not (block-component-editing?)))
+                (cursor/move-cursor-to input pos))
+
+              (when (or (util/mobile?) (mobile-util/native-platform?))
+                (set-state! :mobile/show-action-bar? false)))))))))
 
 (defn action-bar-open?
   []
@@ -2126,10 +2131,10 @@ Similar to re-frame subscriptions"
   ([theme?] (get-enabled?-installed-plugins theme? true false false))
   ([theme? enabled? include-unpacked? include-all?]
    (filterv
-     #(and (if include-unpacked? true (:iir %))
-           (if-not (boolean? enabled?) true (= (not enabled?) (boolean (get-in % [:settings :disabled]))))
-           (or include-all? (if (boolean? theme?) (= (boolean theme?) (:theme %)) true)))
-     (vals (:plugin/installed-plugins @state)))))
+    #(and (if include-unpacked? true (:iir %))
+          (if-not (boolean? enabled?) true (= (not enabled?) (boolean (get-in % [:settings :disabled]))))
+          (or include-all? (if (boolean? theme?) (= (boolean theme?) (:theme %)) true)))
+    (vals (:plugin/installed-plugins @state)))))
 
 (defn lsp-enabled?-or-theme
   []
@@ -2194,7 +2199,6 @@ Similar to re-frame subscriptions"
     (->> (sub :sidebar/blocks)
          (filter #(= (first %) current-repo)))))
 
-
 (defn toggle-collapsed-block!
   [block-id]
   (let [current-repo (get-current-repo)]
@@ -2277,7 +2281,7 @@ Similar to re-frame subscriptions"
   [m]
   (update-state! [:graph/parsing-state (get-current-repo)]
                  (if (fn? m) m
-                   (fn [old-value] (merge old-value m)))))
+                     (fn [old-value] (merge old-value m)))))
 
 (defn http-proxy-enabled-or-val? []
   (when-let [{:keys [type protocol host port] :as agent-opts} (sub [:electron/user-cfgs :settings/agent])]

+ 9 - 7
src/main/frontend/ui.cljs

@@ -1216,16 +1216,17 @@
     "Use current time")])
 
 (rum/defc nlp-calendar
-  [{:keys [selected on-select] :as opts}]
-  (let [on-select' (if (:datetime? opts)
+  [{:keys [selected on-select on-day-click] :as opts}]
+  (let [default-on-select (or on-select on-day-click)
+        on-select' (if (:datetime? opts)
                      (fn [date value]
                        (let [value (or (and (string? value) value)
                                        (.-value (gdom/getElement "time-picker")))]
                          (let [[h m] (string/split value ":")]
                            (when selected
                              (.setHours date h m 0))
-                           (on-select date))))
-                     on-select)]
+                           (default-on-select date))))
+                     default-on-select)]
     [:div.flex.flex-col.gap-2
      (single-calendar (assoc opts :on-select on-select'))
      (when (:datetime? opts)
@@ -1248,10 +1249,11 @@
                       (when (= "Enter" (util/ekey e))
                         (let [value (util/evalue e)]
                           (when-not (string/blank? value)
-                            (when-let [result (date/nld-parse value)]
-                              (when-let [date (doto (goog.date.DateTime.) (.setTime (.getTime result)))]
+                            (let [result (date/nld-parse value)]
+                              (if-let [date (and result (doto (goog.date.DateTime.) (.setTime (.getTime result))))]
                                 (let [on-select' (or (:on-select opts) (:on-day-click opts))]
-                                  (on-select' date))))))))})]))
+                                  (on-select' date))
+                                (notification/show! (str (pr-str value) " is not a valid date. Please try again") :warning)))))))})]))
 
 (comment
   (rum/defc emoji-picker

+ 6 - 7
src/main/frontend/util.cljc

@@ -1509,13 +1509,6 @@ Arg *stop: atom, reset to true to stop the loop"
   [pred coll]
   `(vec (remove ~pred ~coll)))
 
-#?(:cljs
-   (defn safe-with-meta
-     [o meta]
-     (if (satisfies? IMeta o)
-       (with-meta o meta)
-       o)))
-
 ;; from rum
 #?(:cljs
    (def schedule
@@ -1543,3 +1536,9 @@ Arg *stop: atom, reset to true to stop the loop"
                 (js->clj)
                 (into {})
                 (walk/keywordize-keys)))))))
+
+#?(:cljs
+   (defn get-cm-instance
+     [^js target]
+     (when target
+       (some-> target (.querySelector ".CodeMirror") (.-CodeMirror)))))

+ 27 - 2
src/main/frontend/worker/db/migrate.cljs

@@ -9,7 +9,8 @@
             [frontend.worker.search :as search]
             [cljs-bean.core :as bean]
             [logseq.db.sqlite.util :as sqlite-util]
-            [logseq.common.config :as common-config]))
+            [logseq.common.config :as common-config]
+            [logseq.common.util :as common-util]))
 
 ;; TODO: fixes/rollback
 
@@ -96,6 +97,28 @@
                               [:db/add id new prop-value]]))))
             old-new-props)))
 
+(defn- rename-properties
+  [props-to-rename]
+  (fn [conn _search-db]
+    (when (ldb/db-based-graph? @conn)
+      (let [props-tx (mapv (fn [[old new]]
+                             (merge {:db/id (:db/id (d/entity @conn old))
+                                     :db/ident new}
+                                    (when-let [new-title (get-in db-property/built-in-properties [new :title])]
+                                      {:block/title new-title
+                                       :block/name (common-util/page-name-sanity-lc new-title)})))
+                           props-to-rename)]
+       ;; Property changes need to be in their own tx for subsequent uses of properties to take effect
+        (ldb/transact! conn props-tx {:db-migrate? true})
+
+        (mapcat (fn [[old new]]
+                 ;; can't use datoms b/c user properties aren't indexed
+                  (->> (d/q '[:find ?b ?prop-v :in $ ?prop :where [?b ?prop ?prop-v]] @conn old)
+                       (mapcat (fn [[id prop-value]]
+                                 [[:db/retract id old]
+                                  [:db/add id new prop-value]]))))
+                props-to-rename)))))
+
 (defn- update-block-type-many->one
   [conn _search-db]
   (let [db @conn
@@ -287,7 +310,9 @@
         :fix add-query-property-to-query-tag}]
    [26 {:properties [:logseq.property.node/type]}]
    [27 {:properties [:logseq.property.code/mode]}]
-   [28 {:classes [:logseq.class/Asset]
+   [28 {:fix (rename-properties {:logseq.property.node/type :logseq.property.node/display-type})}]
+   [29 {:properties [:logseq.property.code/lang]}]
+   [30 {:classes [:logseq.class/Asset]
         :properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]

+ 65 - 55
src/main/frontend/worker/handler/page/db_based/page.cljs

@@ -12,7 +12,8 @@
             [logseq.db.frontend.property.util :as db-property-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.block :as gp-block]
-            [logseq.graph-parser.text :as text]))
+            [logseq.graph-parser.text :as text]
+            [logseq.outliner.validate :as outliner-validate]))
 
 (defn- build-page-tx [conn properties page {:keys [whiteboard? class? tags]}]
   (when (:block/uuid page)
@@ -86,64 +87,67 @@
    (d/entity db)))
 
 (defn- split-namespace-pages
-  [db page tags date-formatter & {:keys [*changed-uuids]}]
-  (let [tags-set (set (map :block/uuid tags))]
+  [db page date-formatter]
+  (let [{:block/keys [title] block-uuid :block/uuid block-type :block/type} page]
     (->>
-     (let [{:block/keys [title]} page
-           block-uuid (:block/uuid page)
-           block-type (if (contains? tags-set (:block/uuid page))
-                        "class"
-                        (:block/type page))]
-       (if (and (contains? #{"page" "class"} block-type) (ns-util/namespace-page? title))
-         (let [class? (= block-type "class")
-               parts (->> (string/split title ns-util/parent-re)
-                          (map string/trim)
-                          (remove string/blank?))
-               pages (doall
-                      (map-indexed
-                       (fn [idx part]
-                         (let [last-part? (= idx (dec (count parts)))
-                               page (if (zero? idx)
-                                      (ldb/get-page db part)
-                                      (get-page-by-parent-name db (nth parts (dec idx)) part))
-                               result (or page
-                                          (when last-part? (ldb/get-page db part))
-                                          (-> (gp-block/page-name->map part db true date-formatter
-                                                                       {:page-uuid (when last-part? block-uuid)})
-                                              (assoc :block/format :markdown)))]
-                           (when (and last-part? (not= (:block/uuid result) block-uuid)
-                                      *changed-uuids)
-                             (swap! *changed-uuids assoc block-uuid (:block/uuid result)))
-                           result))
-                       parts))]
-           (cond
-             (and (not class?) (ldb/class? (first pages)))
-             [page]
+     (if (and (contains? #{"page" "class"} block-type) (ns-util/namespace-page? title))
+       (let [class? (= block-type "class")
+             parts (->> (string/split title ns-util/parent-re)
+                        (map string/trim)
+                        (remove string/blank?))
+             pages (doall
+                    (map-indexed
+                     (fn [idx part]
+                       (let [last-part? (= idx (dec (count parts)))
+                             page (if (zero? idx)
+                                    (ldb/get-page db part)
+                                    (get-page-by-parent-name db (nth parts (dec idx)) part))
+                             result (or page
+                                        (when last-part? (ldb/get-page db part))
+                                        (-> (gp-block/page-name->map part db true date-formatter
+                                                                     {:page-uuid (when last-part? block-uuid)
+                                                                      :skip-existing-page-check? true
+                                                                      :class? class?})
+                                            (assoc :block/format :markdown)))]
+                         result))
+                     parts))]
+         (cond
+           (and (not class?) (not (every? ldb/internal-page? pages)))
+           (throw (ex-info "Cannot create this page unless all parents are pages"
+                           {:type :notification
+                            :payload {:message "Cannot create this page unless all parents are pages"
+                                      :type :warning}}))
 
-             :else
-             (map-indexed
-              (fn [idx page]
-                (let [parent-eid (when (> idx 0)
-                                   (when-let [id (:block/uuid (nth pages (dec idx)))]
-                                     [:block/uuid id]))]
-                  (if class?
-                    (cond
-                      (and (de/entity? page) (ldb/class? page))
-                      (assoc page :logseq.property/parent parent-eid)
+           (and class? (not (every? ldb/class? pages)))
+           (throw (ex-info "Cannot create this tag unless all parents are tags"
+                           {:type :notification
+                            :payload {:message "Cannot create this tag unless all parents are tags"
+                                      :type :warning}}))
 
-                      (de/entity? page) ; page exists but not a class, avoid converting here because this could be troublesome.
-                      nil
+           :else
+           (map-indexed
+            (fn [idx page]
+              (let [parent-eid (when (> idx 0)
+                                 (when-let [id (:block/uuid (nth pages (dec idx)))]
+                                   [:block/uuid id]))]
+                (if class?
+                  (cond
+                    (and (de/entity? page) (ldb/class? page))
+                    (assoc page :logseq.property/parent parent-eid)
 
-                      (zero? idx)
-                      (db-class/build-new-class db page)
+                    (de/entity? page) ; page exists but not a class, avoid converting here because this could be troublesome.
+                    nil
 
-                      :else
-                      (db-class/build-new-class db (assoc page :logseq.property/parent parent-eid)))
-                    (if (or (de/entity? page) (zero? idx))
-                      page
-                      (assoc page :logseq.property/parent parent-eid)))))
-              pages)))
-         [page]))
+                    (zero? idx)
+                    (db-class/build-new-class db page)
+
+                    :else
+                    (db-class/build-new-class db (assoc page :logseq.property/parent parent-eid)))
+                  (if (or (de/entity? page) (zero? idx))
+                    page
+                    (assoc page :logseq.property/parent parent-eid)))))
+            pages)))
+       [page])
      (remove nil?))))
 
 (defn create!
@@ -173,10 +177,16 @@
                                                     :skip-existing-page-check? true})
                           (assoc :block/format format))
             [page parents] (if (and (text/namespace-page? title) split-namespace?)
-                             (let [pages (split-namespace-pages db page nil date-formatter)]
+                             (let [pages (split-namespace-pages db page date-formatter)]
                                [(last pages) (butlast pages)])
                              [page nil])]
         (when page
+          ;; Don't validate journal names because they can have '/'
+          (when (not= "journal" type)
+            (outliner-validate/validate-page-title-characters (str (:block/title page)) {:node page})
+            (doseq [parent parents]
+              (outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent})))
+
           (let [page-uuid (:block/uuid page)
                 page-txs  (build-page-tx conn properties page (select-keys options [:whiteboard? :class? :tags]))
                 first-block-tx (when (and

+ 66 - 63
src/main/frontend/worker/search.cljs

@@ -1,7 +1,6 @@
 (ns frontend.worker.search
   "Full-text and fuzzy search"
   (:require [clojure.string :as string]
-            [promesa.core :as p]
             [cljs-bean.core :as bean]
             ["fuse.js" :as fuse]
             [goog.object :as gobj]
@@ -136,31 +135,35 @@ DROP TRIGGER IF EXISTS blocks_au;
                         (string/replace " or " " OR ")
                         (string/replace " | " " OR ")
                         (string/replace " not " " NOT "))]
-    (if (not= q match-input)
+    (cond
+      (re-find #"[^\w\s]" q)            ; punctuations
+      (str "\"" match-input "\"*")
+      (not= q match-input)
       (string/replace match-input "," "")
-      (str "\"" match-input "\"*"))))
+      :else
+      match-input)))
 
 (defn- search-blocks-aux
   [db sql q input page limit enable-snippet?]
   (try
-    (p/let [namespace? (ns-util/namespace-page? q)
-            last-part (when namespace?
-                        (some-> (text/get-namespace-last-part q)
-                                get-match-input))
-            bind (cond
-                   (and namespace? page)
-                   [page input last-part limit]
-                   page
-                   [page input limit]
-                   namespace?
-                   [input last-part limit]
-                   :else
-                   [input limit])
-            result (.exec db (bean/->js
-                              {:sql sql
-                               :bind bind
-                               :rowMode "array"}))
-            blocks (bean/->clj result)]
+    (let [namespace? (ns-util/namespace-page? q)
+          last-part (when namespace?
+                      (some-> (text/get-namespace-last-part q)
+                              get-match-input))
+          bind (cond
+                 (and namespace? page)
+                 [page input last-part limit]
+                 page
+                 [page input limit]
+                 namespace?
+                 [input last-part limit]
+                 :else
+                 [input limit])
+          result (.exec db (bean/->js
+                            {:sql sql
+                             :bind bind
+                             :rowMode "array"}))
+          blocks (bean/->clj result)]
       (map (fn [block]
              (let [[id page title snippet] (if enable-snippet?
                                              (update block 3 get-snippet-result)
@@ -241,7 +244,7 @@ DROP TRIGGER IF EXISTS blocks_au;
                                 :distance 1024
                                 :threshold 0.5 ;; search for 50% match from the start
                                 :minMatchCharLength 1}))]
-    (swap! fuzzy-search-indices assoc-in repo indice)
+    (swap! fuzzy-search-indices assoc repo indice)
     indice))
 
 (defn fuzzy-search
@@ -273,51 +276,51 @@ DROP TRIGGER IF EXISTS blocks_au;
                           :as option
                           :or {enable-snippet? true}}]
   (when-not (string/blank? q)
-    (p/let [match-input (get-match-input q)
-            limit  (or limit 100)
+    (let [match-input (get-match-input q)
+          limit  (or limit 100)
             ;; https://www.sqlite.org/fts5.html#the_highlight_function
             ;; the 2nd column in blocks_fts (content)
             ;; pfts_2lqh is a key for retrieval
             ;; highlight and snippet only works for some matching with high rank
-            snippet-aux "snippet(blocks_fts, 1, '$pfts_2lqh>$', '$<pfts_2lqh$', '...', 32)"
-            select (if enable-snippet?
-                     (str "select id, page, title, " snippet-aux " from blocks_fts where ")
-                     "select id, page, title from blocks_fts where ")
-            pg-sql (if page "page = ? and" "")
-            match-sql (if (ns-util/namespace-page? q)
-                        (str select pg-sql " title match ? or title match ? order by rank limit ?")
-                        (str select pg-sql " title match ? order by rank limit ?"))
-            matched-result (search-blocks-aux search-db match-sql q match-input page limit enable-snippet?)
-            fuzzy-result (when-not page (fuzzy-search repo @conn q option))]
-      (let [result (->> (concat fuzzy-result matched-result)
-                        (common-util/distinct-by :id)
-                        (keep (fn [result]
-                                (let [{:keys [id page title snippet]} result
-                                      block-id (uuid id)]
-                                  (when-let [block (d/entity @conn [:block/uuid block-id])]
-                                    (when (if dev?
-                                            true
-                                            (if built-in?
-                                              (or (not (ldb/built-in? block))
-                                                  (not (ldb/private-built-in-page? block))
-                                                  (ldb/class? block))
-                                              (or (not (ldb/built-in? block))
-                                                  (ldb/class? block))))
-                                      {:db/id (:db/id block)
-                                       :block/uuid block-id
-                                       :block/title (if (ldb/page? block)
-                                                      (ldb/get-title-with-parents block)
-                                                      (or snippet title))
-                                       :block/page (if (common-util/uuid-string? page)
-                                                     (uuid page)
-                                                     nil)
-                                       :block/tags (seq (map :db/id (:block/tags block)))
-                                       :page? (ldb/page? block)}))))))
-            page-or-object-result (filter (fn [b] (or (:page? b) (:block/tags result))) result)]
-        (->>
-         (concat page-or-object-result
-                 (remove (fn [b] (or (:page? b) (:block/tags result))) result))
-         (common-util/distinct-by :block/uuid))))))
+          snippet-aux "snippet(blocks_fts, 1, '$pfts_2lqh>$', '$<pfts_2lqh$', '...', 256)"
+          select (if enable-snippet?
+                   (str "select id, page, title, " snippet-aux " from blocks_fts where ")
+                   "select id, page, title from blocks_fts where ")
+          pg-sql (if page "page = ? and" "")
+          match-sql (if (ns-util/namespace-page? q)
+                      (str select pg-sql " title match ? or title match ? order by rank limit ?")
+                      (str select pg-sql " title match ? order by rank limit ?"))
+          matched-result (search-blocks-aux search-db match-sql q match-input page limit enable-snippet?)
+          fuzzy-result (when-not page (fuzzy-search repo @conn q option))
+          result (->> (concat fuzzy-result matched-result)
+                      (common-util/distinct-by :id)
+                      (keep (fn [result]
+                              (let [{:keys [id page title snippet]} result
+                                    block-id (uuid id)]
+                                (when-let [block (d/entity @conn [:block/uuid block-id])]
+                                  (when (if dev?
+                                          true
+                                          (if built-in?
+                                            (or (not (ldb/built-in? block))
+                                                (not (ldb/private-built-in-page? block))
+                                                (ldb/class? block))
+                                            (or (not (ldb/built-in? block))
+                                                (ldb/class? block))))
+                                    {:db/id (:db/id block)
+                                     :block/uuid block-id
+                                     :block/title (if (ldb/page? block)
+                                                    (ldb/get-title-with-parents block)
+                                                    (or snippet title))
+                                     :block/page (if (common-util/uuid-string? page)
+                                                   (uuid page)
+                                                   nil)
+                                     :block/tags (seq (map :db/id (:block/tags block)))
+                                     :page? (ldb/page? block)}))))))
+          page-or-object-result (filter (fn [b] (or (:page? b) (:block/tags result))) result)]
+      (->>
+       (concat page-or-object-result
+               (remove (fn [b] (or (:page? b) (:block/tags result))) result))
+       (common-util/distinct-by :block/uuid)))))
 
 (defn truncate-table!
   [db]

+ 38 - 44
src/test/frontend/components/query/result_test.cljs

@@ -1,20 +1,14 @@
 (ns frontend.components.query.result-test
   (:require [clojure.test :refer [deftest are testing is]]
-            [rum.core :as rum]
-            [frontend.db.query-custom :as query-custom]
             [frontend.db.model :as model]
             [frontend.components.query.result :as query-result]))
 
-(defn- mock-get-query-result
-  "Mocks get-query-result assuming custom queries are being tested. Db calls are
-  mocked to minimize setup"
-  [result query-m {:keys [table? current-block-uuid config] :or {config {}}}]
-  (with-redefs [query-custom/custom-query (constantly (atom result))
-                model/with-pages identity]
-    (binding [rum/*reactions* (volatile! #{})]
-      (#'query-result/get-query-result config query-m (atom nil) (atom nil) current-block-uuid {:table? table?}))))
+(defn- transform-query-result
+  [config query-m result]
+  (with-redefs [model/with-pages identity]
+    (query-result/transform-query-result config query-m result)))
 
-(deftest get-query-result-with-transforms-and-grouping
+(deftest transform-query-result-with-transforms-and-grouping
   (let [result (mapv
                 #(assoc % :block/page {:db/id 1} :block/parent {:db/id 2})
                 [{:block/uuid (random-uuid) :block/scheduled 20230418}
@@ -23,63 +17,63 @@
         sorted-result (sort-by :block/scheduled result)]
     (testing "For list view"
       (are [query-m expected]
-           (= expected (mock-get-query-result result query-m {:table? false}))
+           (= expected (transform-query-result {:table? false} query-m result))
 
-           ;; Default list behavior is to group result
-           {}
-           {{:db/id 1} result}
+        ;; Default list behavior is to group result
+        {}
+        {{:db/id 1} result}
 
-           ;; User overrides default behavior to return result
-           {:group-by-page? false}
-           result
+        ;; User overrides default behavior to return result
+        {:group-by-page? false}
+        result
 
-           ;; Return transformed result for list view
-           {:result-transform '(partial sort-by :block/scheduled)}
-           sorted-result
+        ;; Return transformed result for list view
+        {:result-transform '(partial sort-by :block/scheduled)}
+        sorted-result
 
-           ; User overrides transform to return grouped result
-           {:result-transform '(partial sort-by :block/scheduled) :group-by-page? true}
-           {{:db/id 1} sorted-result})
+        ; User overrides transform to return grouped result
+        {:result-transform '(partial sort-by :block/scheduled) :group-by-page? true}
+        {{:db/id 1} sorted-result})
 
       (testing "For table view"
         (are [query expected]
-             (= expected (mock-get-query-result result query {:table? true}))
+             (= expected (transform-query-result {:table? true} query result))
 
-             ;; Default table behavior is to return result
-             {}
-             result
+          ;; Default table behavior is to return result
+          {}
+          result
 
-             ;; Return transformed result
-             {:result-transform '(partial sort-by :block/scheduled)}
-             sorted-result
+          ;; Return transformed result
+          {:result-transform '(partial sort-by :block/scheduled)}
+          sorted-result
 
-             ;; Ignore override and return normal result
-             {:group-by-page? true}
-             result))
+          ;; Ignore override and return normal result
+          {:group-by-page? true}
+          result))
 
       (testing "current block in results"
         (is (= result
                (let [current-block {:block/uuid (random-uuid) :block/scheduled 20230420 :block/page {:db/id 1}}]
-                 (mock-get-query-result (conj result current-block)
-                                        {:group-by-page? false}
-                                        {:table? false
-                                         :current-block-uuid (:block/uuid current-block)})))
+                 (transform-query-result {:table? false
+                                          :current-block-uuid (:block/uuid current-block)}
+                                         {:group-by-page? false}
+                                         (conj result current-block))))
             "Current block is not included in results")))))
 
-(deftest get-query-result-with-remove-block-children-option
+(deftest transform-query-result-with-remove-block-children-option
   (let [result [{:db/id 1 :block/title "parent" :block/uuid 1}
                 {:db/id 2 :block/title "child" :block/uuid 2 :block/parent {:db/id 1}}]]
     (is (= [{:db/id 1 :block/title "parent" :block/uuid 1}]
-           (mock-get-query-result result {:remove-block-children? true} {:table? true}))
+           (transform-query-result {:table? true} {:remove-block-children? true} result))
         "Removes children when :remove-block-children? is true")
     (is (= result
-           (mock-get-query-result result {:remove-block-children? false} {:table? true}))
+           (transform-query-result {:table? true} {:remove-block-children? false} result))
         "Doesn't remove children when :remove-block-children? is false")))
 
-(deftest get-query-result-sets-result-in-config
+(deftest transform-query-result-sets-result-in-config
   (let [result [{:db/id 1 :block/title "parent" :block/uuid 1}]
-        config {:query-result (atom nil)}]
+        config {:query-result (atom nil) :table? true}]
     (is (= result
-           (mock-get-query-result result {} {:table? true :config config})))
+           (transform-query-result config {} result)))
     (is (= result @(:query-result config))
         "Result is set in config for downstream use e.g. query table fn")))

+ 79 - 0
src/test/frontend/worker/handler/page/db_based/page_test.cljs

@@ -0,0 +1,79 @@
+(ns frontend.worker.handler.page.db-based.page-test
+  (:require [cljs.test :refer [deftest is testing]]
+            [datascript.core :as d]
+            [logseq.db.test.helper :as db-test]
+            [logseq.db :as ldb]
+            [frontend.worker.handler.page.db-based.page :as worker-db-page]))
+
+(deftest create-class
+  (let [conn (db-test/create-conn)
+        _ (worker-db-page/create! conn "movie" {:class? true})
+        _ (worker-db-page/create! conn "Movie" {:class? true})
+        movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]]
+                              @conn "movie")
+                         first)
+        Movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]]
+                              @conn "Movie")
+                         first)]
+
+    (is (ldb/class? movie-class) "Creates a class")
+    (is (ldb/class? Movie-class) "Creates another class with a different case sensitive name")
+    (is (not= movie-class Movie-class) "The two classes are not the same")))
+
+(deftest create-namespace-pages
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:property1 {:block/schema {:type :default}}}
+               :classes {:class1 {}}
+               :pages-and-blocks [{:page {:block/title "page1"}}]})]
+
+    (testing "Valid workflows"
+      (let [[_ child-uuid] (worker-db-page/create! conn "foo/bar/baz" {:split-namespace? true})
+            child-page (d/entity @conn [:block/uuid child-uuid])
+            ;; Create a 2nd child page using existing parent pages
+            [_ child-uuid2] (worker-db-page/create! conn "foo/bar/baz2" {:split-namespace? true})
+            child-page2 (d/entity @conn [:block/uuid child-uuid2])
+            ;; Create a child page for a class
+            [_ child-uuid3] (worker-db-page/create! conn "c1/c2" {:split-namespace? true :class? true})
+            child-page3 (d/entity @conn [:block/uuid child-uuid3])]
+        (is (= ["foo" "bar"] (map :block/title (ldb/get-page-parents child-page)))
+            "Child page with new parent has correct parents")
+        (is (= (map :block/uuid (ldb/get-page-parents child-page))
+               (map :block/uuid (ldb/get-page-parents child-page2)))
+            "Child page with existing parents has correct parents")
+        (is (= ["Root Tag" "c1"] (map :block/title (ldb/get-classes-parents [child-page3])))
+            "Child class with new parent has correct parents")
+
+        (worker-db-page/create! conn "foo/class1/baz3" {:split-namespace? true})
+        (is (= #{"class" "page"}
+               (set (d/q '[:find [?type ...]
+                           :where [?b :block/type ?type] [?b :block/title "class1"]] @conn)))
+            "Using an existing class page in a multi-parent namespace doesn't allow a page to have a class parent and instead creates a new page")))
+
+    (testing "Invalid workflows"
+      (is (thrown-with-msg?
+           js/Error
+           #"Cannot create"
+           (worker-db-page/create! conn "class1/page" {:split-namespace? true}))
+          "Page can't have a class parent")
+      (is (thrown-with-msg?
+           js/Error
+           #"Cannot create"
+           (worker-db-page/create! conn "property1/page" {:split-namespace? true}))
+          "Page can't have a property parent")
+      (is (thrown-with-msg?
+           js/Error
+           #"Cannot create"
+           (worker-db-page/create! conn "property1/class" {:split-namespace? true :class? true}))
+          "Class can't have a property parent"))))
+
+(deftest create-page
+  (let [conn (db-test/create-conn)
+        [_ page-uuid] (worker-db-page/create! conn "fooz" {})]
+    (is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
+        "Valid page created")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"can't include \"/"
+         (worker-db-page/create! conn "foo/bar" {}))
+        "Page can't have '/'n title")))

+ 1 - 0
tailwind.all.css

@@ -21,4 +21,5 @@
 @import "pdfjs-dist/web/pdf_viewer.css";
 @import "resources/css/tabler-extension.css";
 @import "resources/css/codemirror.lsradix.css";
+@import "@tabler/icons-webfont/tabler-icons.min.css";
 @import-glob "src/main/frontend/**/[!_]*.css";

+ 0 - 0
validate.cljs


+ 76 - 15
yarn.lock

@@ -942,10 +942,25 @@
   dependencies:
     defer-to-connect "^2.0.0"
 
-"@tabler/icons@^1.96.0":
-  version "1.119.0"
-  resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.119.0.tgz#8c590bc5a563c8673a78ccd451bedabd584b376e"
-  integrity sha512-Fk3Qq4w2SXcTjc/n1cuL5bccPkylrOMo7cYpQIf/yw6zP76LQV9dtLcHQUjFiUnaYuswR645CnURIhlafyAh9g==
+"@tabler/icons-react@^2.47.0":
+  version "2.47.0"
+  resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-2.47.0.tgz#b704e7ae98f95be8bd6e938b4b2e84cd20b0cf31"
+  integrity sha512-iqly2FvCF/qUbgmvS8E40rVeYY7laltc5GUjRxQj59DuX0x/6CpKHTXt86YlI2whg4czvd/c8Ce8YR08uEku0g==
+  dependencies:
+    "@tabler/icons" "2.47.0"
+    prop-types "^15.7.2"
+
+"@tabler/icons-webfont@^2.47.0":
+  version "2.47.0"
+  resolved "https://registry.yarnpkg.com/@tabler/icons-webfont/-/icons-webfont-2.47.0.tgz#4b4303170d1ed616140044848d6ef99cc993abf9"
+  integrity sha512-yfV9zDal0iYDmyGz4BS9IlhaaMydtLdyOrY2UAZToP65sVWj7AFIi6symNzsoBaX867xAZWVHdKcocah0BfSog==
+  dependencies:
+    "@tabler/icons" "2.47.0"
+
+"@tabler/[email protected]":
+  version "2.47.0"
+  resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.47.0.tgz#c41c680d1947e3ab2d60af3febc4132287c60596"
+  integrity sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==
 
 "@tailwindcss/[email protected]":
   version "0.4.2"
@@ -1149,6 +1164,14 @@
     "@types/expect" "^1.20.4"
     "@types/node" "*"
 
+"@types/vinyl@^2.0.4":
+  version "2.0.12"
+  resolved "https://registry.yarnpkg.com/@types/vinyl/-/vinyl-2.0.12.tgz#17642ca9a8ae10f3db018e9f885da4188db4c6e6"
+  integrity sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==
+  dependencies:
+    "@types/expect" "^1.20.4"
+    "@types/node" "*"
+
 "@types/yauzl@^2.9.1":
   version "2.10.0"
   resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
@@ -1575,6 +1598,11 @@ binary-extensions@^2.0.0:
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
+binaryextensions@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22"
+  integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==
+
 bindings@^1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
@@ -2106,10 +2134,10 @@ code-point-at@^1.0.0:
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==
 
[email protected]3:
-  version "5.65.13"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.13.tgz#c098a6f409db8b5a7c5722788bd9fa3bb2367f2e"
-  integrity sha512-SVWEzKXmbHmTQQWaz03Shrh4nybG0wXx2MEu3FO4ezbPW8IbnZEd5iGHGEffSUaitKYa3i+pHpBsSvw8sPHtzg==
[email protected]8:
+  version "5.65.18"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.18.tgz#d7146e4271135a9b4adcd023a270185457c9c428"
+  integrity sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==
 
 collection-map@^1.0.0:
   version "1.0.0"
@@ -3102,7 +3130,7 @@ escape-html@~1.0.3:
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
 
-escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
@@ -3956,6 +3984,17 @@ gulp-postcss@^10.0.0:
     postcss-load-config "^5.0.0"
     vinyl-sourcemaps-apply "^0.2.1"
 
+gulp-replace@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/gulp-replace/-/gulp-replace-1.1.4.tgz#06a0e9ee36f30e343c1e0a2dd760ec32c8a3d3b2"
+  integrity sha512-SVSF7ikuWKhpAW4l4wapAqPPSToJoiNKsbDoUnRrSgwZHH7lH8pbPeQj1aOVYQrbZKhfSVBxVW+Py7vtulRktw==
+  dependencies:
+    "@types/node" "*"
+    "@types/vinyl" "^2.0.4"
+    istextorbinary "^3.0.0"
+    replacestream "^4.0.3"
+    yargs-parser ">=5.0.0-security.0"
+
 gulp@^4.0.2:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa"
@@ -4718,6 +4757,14 @@ [email protected]:
   dependencies:
     fast-text-encoding "^1.0.0"
 
+istextorbinary@^3.0.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-3.3.0.tgz#06b1c57d948da11461bd237c00ce09e9902964f2"
+  integrity sha512-Tvq1W6NAcZeJ8op+Hq7tdZ434rqnMx4CCZ7H0ff83uEloDvVbqAwaMTZcafKGJT0VHkYzuXUiCY4hlXQg6WfoQ==
+  dependencies:
+    binaryextensions "^2.2.0"
+    textextensions "^3.2.0"
+
 jiti@^1.19.1:
   version "1.21.0"
   resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
@@ -6754,7 +6801,7 @@ prompts@^2.4.2:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
[email protected], prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1:
[email protected], prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7217,6 +7264,15 @@ replace-homedir@^1.0.0:
     is-absolute "^1.0.0"
     remove-trailing-separator "^1.1.0"
 
+replacestream@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/replacestream/-/replacestream-4.0.3.tgz#3ee5798092be364b1cdb1484308492cb3dff2f36"
+  integrity sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==
+  dependencies:
+    escape-string-regexp "^1.0.3"
+    object-assign "^4.0.1"
+    readable-stream "^2.0.2"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -8212,6 +8268,11 @@ text-segmentation@^1.0.3:
   dependencies:
     utrie "^1.0.2"
 
+textextensions@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-3.3.0.tgz#03530d5287b86773c08b77458589148870cc71d3"
+  integrity sha512-mk82dS8eRABNbeVJrEiN5/UMSCliINAuz8mkUwH4SwslkNP//gbEzlWNS5au0z5Dpx40SQxzqZevZkn+WYJ9Dw==
+
 thenby@^1.3.4:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/thenby/-/thenby-1.3.4.tgz#81581f6e1bb324c6dedeae9bfc28e59b1a2201cc"
@@ -9016,6 +9077,11 @@ [email protected]:
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
   integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
 
+yargs-parser@>=5.0.0-security.0, yargs-parser@^21.1.1:
+  version "21.1.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
 yargs-parser@^11.1.1:
   version "11.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
@@ -9029,11 +9095,6 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3:
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
   integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
 
-yargs-parser@^21.1.1:
-  version "21.1.1"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
-  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
-
 yargs-parser@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.1.tgz#7ede329c1d8cdbbe209bd25cdb990e9b1ebbb394"

Some files were not shown because too many files changed in this diff