Ver código fonte

Merge branch 'feat/db' into refactor/block-schema

Tienson Qin 9 meses atrás
pai
commit
e318e0d6c1
100 arquivos alterados com 2176 adições e 1728 exclusões
  1. 10 3
      .clj-kondo/config.edn
  2. 27 0
      .clj-kondo/hooks/defkeywords.clj
  3. 1 1
      deps.edn
  4. 2 1
      deps/common/.carve/config.edn
  5. 35 0
      deps/common/src/logseq/common/defkeywords.cljc
  6. 17 8
      deps/common/src/logseq/common/util/date_time.cljs
  7. 5 2
      deps/db/script/validate_client_db.cljs
  8. 1 1
      deps/db/src/logseq/db.cljs
  9. 73 72
      deps/db/src/logseq/db/frontend/class.cljs
  10. 1 2
      deps/db/src/logseq/db/frontend/entity_util.cljs
  11. 24 0
      deps/db/src/logseq/db/frontend/kv_entity.cljs
  12. 59 34
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  13. 473 451
      deps/db/src/logseq/db/frontend/property.cljs
  14. 1 1
      deps/db/src/logseq/db/frontend/schema.cljs
  15. 6 7
      deps/db/src/logseq/db/frontend/validate.cljs
  16. 1 1
      deps/db/src/logseq/db/sqlite/common_db.cljs
  17. 7 7
      deps/db/test/logseq/db/sqlite/create_graph_test.cljs
  18. 31 17
      deps/graph-parser/script/db_import.cljs
  19. 40 28
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  20. 16 10
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  21. 2 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_11_26.md
  22. 1 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2025_01_10.md
  23. 1 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/Property.md
  24. 1 1
      deps/graph-parser/test/resources/exporter-test-graph/pages/url.md
  25. 4 6
      deps/outliner/src/logseq/outliner/pipeline.cljs
  26. 1 1
      libs/package.json
  27. 10 1
      libs/src/LSPlugin.core.ts
  28. 12 1
      libs/src/LSPlugin.ts
  29. 97 72
      libs/src/LSPlugin.user.ts
  30. 16 3
      libs/src/modules/LSPlugin.Experiments.ts
  31. 0 27
      resources/js/lsplugin.core.js
  32. 1 1
      resources/js/lsplugin.core.js.LICENSE.txt
  33. 5 3
      scripts/src/logseq/tasks/dev/db_and_file_graphs.clj
  34. 0 13
      src/main/frontend/common/schema_register.clj
  35. 0 23
      src/main/frontend/common/schema_register.cljs
  36. 10 48
      src/main/frontend/common_keywords.cljs
  37. 5 4
      src/main/frontend/components/all_pages.cljs
  38. 16 17
      src/main/frontend/components/assets.cljs
  39. 52 51
      src/main/frontend/components/block.cljs
  40. 15 14
      src/main/frontend/components/bug_report.cljs
  41. 8 7
      src/main/frontend/components/cmdk/core.cljs
  42. 31 30
      src/main/frontend/components/cmdk/list_item.cljs
  43. 12 12
      src/main/frontend/components/container.cljs
  44. 6 5
      src/main/frontend/components/dnd.cljs
  45. 11 9
      src/main/frontend/components/editor.cljs
  46. 4 3
      src/main/frontend/components/file_based/block.cljs
  47. 18 17
      src/main/frontend/components/file_sync.cljs
  48. 8 7
      src/main/frontend/components/git.cljs
  49. 8 7
      src/main/frontend/components/handbooks.cljs
  50. 5 4
      src/main/frontend/components/header.cljs
  51. 15 14
      src/main/frontend/components/icon.cljs
  52. 2 1
      src/main/frontend/components/imports.cljs
  53. 3 2
      src/main/frontend/components/objects.cljs
  54. 6 5
      src/main/frontend/components/page.cljs
  55. 67 31
      src/main/frontend/components/plugins.cljs
  56. 2 1
      src/main/frontend/components/plugins_settings.cljs
  57. 9 4
      src/main/frontend/components/property.cljs
  58. 35 23
      src/main/frontend/components/property/config.cljs
  59. 14 20
      src/main/frontend/components/property/value.cljs
  60. 6 5
      src/main/frontend/components/query.cljs
  61. 4 3
      src/main/frontend/components/query/builder.cljs
  62. 3 2
      src/main/frontend/components/right_sidebar.cljs
  63. 9 8
      src/main/frontend/components/server.cljs
  64. 2 1
      src/main/frontend/components/settings.cljs
  65. 1 1
      src/main/frontend/components/settings.css
  66. 6 5
      src/main/frontend/components/shortcut.cljs
  67. 15 14
      src/main/frontend/components/theme.cljs
  68. 59 58
      src/main/frontend/components/user/login.cljs
  69. 10 8
      src/main/frontend/components/views.cljs
  70. 1 2
      src/main/frontend/core.cljs
  71. 1 6
      src/main/frontend/date.cljs
  72. 1 1
      src/main/frontend/db/async.cljs
  73. 164 162
      src/main/frontend/extensions/handbooks/core.cljs
  74. 53 49
      src/main/frontend/extensions/pdf/core.cljs
  75. 34 33
      src/main/frontend/extensions/pdf/toolbar.cljs
  76. 5 3
      src/main/frontend/extensions/pdf/utils.cljs
  77. 5 4
      src/main/frontend/extensions/pdf/windows.cljs
  78. 17 16
      src/main/frontend/extensions/tldraw.cljs
  79. 2 1
      src/main/frontend/extensions/zotero.cljs
  80. 9 9
      src/main/frontend/handler/common.cljs
  81. 25 24
      src/main/frontend/handler/common/plugin.cljs
  82. 3 3
      src/main/frontend/handler/editor.cljs
  83. 5 4
      src/main/frontend/handler/events.cljs
  84. 25 24
      src/main/frontend/handler/import.cljs
  85. 39 0
      src/main/frontend/handler/plugin.cljs
  86. 23 11
      src/main/frontend/handler/profiler.cljs
  87. 55 0
      src/main/frontend/hooks.cljs
  88. 3 12
      src/main/frontend/mixins.cljs
  89. 23 22
      src/main/frontend/mobile/graph_picker.cljs
  90. 5 4
      src/main/frontend/rum.cljs
  91. 1 1
      src/main/frontend/state.cljs
  92. 9 8
      src/main/frontend/ui.cljs
  93. 3 14
      src/main/frontend/util.cljc
  94. 13 7
      src/main/frontend/worker/commands.cljs
  95. 161 32
      src/main/frontend/worker/db/migrate.cljs
  96. 7 6
      src/main/frontend/worker/db/validate.cljs
  97. 0 16
      src/main/frontend/worker/db_listener.cljs
  98. 3 1
      src/main/frontend/worker/db_worker.cljs
  99. 5 2
      src/main/frontend/worker/handler/page.cljs
  100. 18 11
      src/main/frontend/worker/handler/page/db_based/page.cljs

+ 10 - 3
.clj-kondo/config.edn

@@ -12,6 +12,7 @@
 
  :linters
  {:path-invalid-construct/string-join {:level :info}
+  :defkeywords/invalid-arg {:level :warning}
   :regex-checks/double-escaped-regex {:level :warning}
   :aliased-namespace-symbol {:level :warning}
   :shadowed-var {:level :warning
@@ -34,7 +35,11 @@
   :discouraged-namespace
   {logseq.db.sqlite.cli {:message "frontend should not depend on CLI namespace with sqlite3 dependency"}
    logseq.outliner.cli {:message "frontend should not depend on CLI namespace with sqlite3 dependency"}}
-
+  :discouraged-var
+  {rum.core/use-effect! {:message "Use frontend.hooks/use-effect! instead" :level :info}
+   rum.core/use-memo {:message "Use frontend.hooks/use-memo instead" :level :info}
+   rum.core/use-layout-effect! {:message "Use frontend.hooks/use-layout-effect! instead" :level :info}
+   rum.core/use-callback {:message "Use frontend.hooks/use-callback instead" :level :info}}
   :unused-namespace {:level :warning
                      :exclude [logseq.db.frontend.entity-plus]}
 
@@ -122,6 +127,7 @@
              frontend.handler.route route-handler
              frontend.handler.search search-handler
              frontend.handler.ui ui-handler
+             frontend.hooks hooks
              frontend.idb idb
              frontend.loader loader
              frontend.mixins mixins
@@ -199,7 +205,8 @@
  :hooks {:analyze-call {rum.core/defc hooks.rum/defc
                         rum.core/defcs hooks.rum/defcs
                         clojure.string/join hooks.path-invalid-construct/string-join
-                        clojure.string/replace hooks.regex-checks/double-escaped-regex}}
+                        clojure.string/replace hooks.regex-checks/double-escaped-regex
+                        logseq.common.defkeywords/defkeywords hooks.defkeywords/defkeywords}}
  :lint-as {promesa.core/let clojure.core/let
            promesa.core/loop clojure.core/loop
            promesa.core/recur clojure.core/recur
@@ -215,7 +222,7 @@
            frontend.test.helper/deftest-async clojure.test/deftest
            frontend.worker.rtc.idb-keyval-mock/with-reset-idb-keyval-mock cljs.test/async
            frontend.react/defc clojure.core/defn
-           frontend.common.schema-register/defkeyword cljs.spec.alpha/def}
+           logseq.common.defkeywords/defkeyword cljs.spec.alpha/def}
  :skip-comments true
  :output {:progress true
           :exclude-files ["src/test/docs-0.10.9/"]}}

+ 27 - 0
.clj-kondo/hooks/defkeywords.clj

@@ -0,0 +1,27 @@
+(ns hooks.defkeywords
+  (:require [clj-kondo.hooks-api :as api]))
+
+(defn defkeywords
+  [{:keys [node]}]
+  (let [[_ & keyvals] (:children node)
+        kw->v (partition 2 keyvals)
+        kws (map first kw->v)]
+    (cond
+      (odd? (count keyvals))
+      (api/reg-finding!
+       (assoc (meta node)
+              :message "Require even number of args"
+              :type :defkeywords/invalid-arg))
+      (not (every? (comp qualified-keyword? api/sexpr) kws))
+      (api/reg-finding!
+       (assoc (meta node)
+              :message "Should use qualified-keywords"
+              :type :defkeywords/invalid-arg))
+      :else
+      (let [new-node (api/list-node
+                      (map (fn [[kw v]]
+                             (api/list-node
+                              [(api/token-node 'logseq.common.defkeywords/defkeyword) kw v]))
+                           kw->v))]
+        {:node (with-meta new-node
+                 (meta node))}))))

+ 1 - 1
deps.edn

@@ -18,7 +18,7 @@
                                          :sha     "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
   ;; TODO: delete cljs-drag-n-drop and use dnd-kit
   cljs-drag-n-drop/cljs-drag-n-drop     {:mvn/version "0.1.0"}
-  cljs-http/cljs-http                   {:mvn/version "0.1.46"}
+  cljs-http/cljs-http                   {:mvn/version "0.1.48"}
   org.babashka/sci                      {:mvn/version "0.3.2"}
   org.clj-commons/hickory               {:mvn/version "0.7.3"}
   hiccups/hiccups                       {:mvn/version "0.3.0"}

+ 2 - 1
deps/common/.carve/config.edn

@@ -9,5 +9,6 @@
                   logseq.common.date
                   logseq.common.util.macro
                   logseq.common.marker
-                  logseq.common.config]
+                  logseq.common.config
+                  logseq.common.defkeywords]
  :report {:format :ignore}}

+ 35 - 0
deps/common/src/logseq/common/defkeywords.cljc

@@ -0,0 +1,35 @@
+(ns logseq.common.defkeywords
+  "Macro 'defkeywords' to def keyword with config"
+  #?(:cljs (:require-macros [logseq.common.defkeywords])))
+
+(def ^:private *defined-kws (volatile! {}))
+(def ^:private *defined-kw->config (volatile! {}))
+
+#_:clj-kondo/ignore
+(defmacro defkeyword
+  "Define keyword with docstring.
+  How 'find keyword definition' works?
+  clojure-lsp treat keywords defined by `cljs.spec.alpha/def` as keyword-definition.
+  Adding a :lint-as `defkeyword` -> `cljs.spec.alpha/def` in clj-kondo config make it works."
+  [& _args])
+
+(defmacro defkeywords
+  "impl at hooks.defkeywords in .clj-kondo
+(defkeywords ::a <config-map> ::b <config-map>)"
+  [& keyvals]
+  (let [kws (take-nth 2 keyvals)
+        current-meta (meta &form)]
+    (doseq [kw kws]
+      (when-let [info (get @*defined-kws kw)]
+        (when (not= (:file current-meta) (:file info))
+          (vswap! *defined-kws assoc kw current-meta)
+          (throw (ex-info "keyword already defined somewhere else" {:kw kw :info info}))))
+      (vswap! *defined-kws assoc kw current-meta))
+    (let [kw->config (partition 2 keyvals)]
+      (doseq [[kw config] kw->config]
+        (vswap! *defined-kw->config assoc kw config))))
+  `(vector ~@keyvals))
+
+(defmacro get-all-defined-kw->config
+  []
+  `'~(deref *defined-kw->config))

+ 17 - 8
deps/common/src/logseq/common/util/date_time.cljs

@@ -1,6 +1,8 @@
 (ns logseq.common.util.date-time
   "cljs-time util fns for deps"
-  (:require [cljs-time.format :as tf]
+  (:require [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [cljs-time.format :as tf]
             [clojure.string :as string]
             [logseq.common.util :as common-util]))
 
@@ -89,10 +91,17 @@
    (string/replace (ymd date) "/" "")))
 
 (defn journal-day->ms
-  "Convert :block/journal-day int to ms timestamp in current timezone"
-  [journal-day]
-  (let [journal-day' (str journal-day)
-        year (js/parseInt (subs journal-day' 0 4))
-        month (dec (js/parseInt (subs journal-day' 4 6)))
-        day (js/parseInt (subs journal-day' 6 8))]
-    (.getTime (new js/Date year month day))))
+  "Converts a journal's :block/journal-day integer into milliseconds"
+  [day]
+  (when day
+    (-> (tf/parse (tf/formatter "yyyyMMdd") (str day))
+        (tc/to-long))))
+
+(defn ms->journal-day
+  "Converts a milliseconds timestamp to the nearest :block/journal-day"
+  [ms]
+  (->> ms
+       tc/from-long
+       t/to-default-time-zone
+       (tf/unparse (tf/formatter "yyyyMMdd"))
+       parse-long))

+ 5 - 2
deps/db/script/validate_client_db.cljs

@@ -20,10 +20,13 @@
   (let [ent-maps (db-malli-schema/update-properties-in-ents db ent-maps*)
         explainer (db-validate/get-schema-explainer closed-maps)]
     (if-let [explanation (binding [db-malli-schema/*db-for-validate-fns* db]
-                           (->> ent-maps explainer not-empty))]
+                           (->> (map (fn [e] (dissoc e :db/id)) ent-maps) explainer not-empty))]
       (do
         (if group-errors
-          (let [ent-errors (db-validate/group-errors-by-entity db ent-maps (:errors explanation))]
+          (let [ent-errors (->> (db-validate/group-errors-by-entity db ent-maps (:errors explanation))
+                                (map #(assoc %
+                                             :dispatch-key
+                                             (->> (dissoc (:entity %) :db/id) (db-malli-schema/entity-dispatch-key db)))))]
             (println "Found" (count ent-errors) "entities in errors:")
             (cond
               verbose

+ 1 - 1
deps/db/src/logseq/db.cljs

@@ -58,7 +58,7 @@
    (transact! repo-or-conn tx-data nil))
   ([repo-or-conn tx-data tx-meta]
    (when (or (exists? js/process)
-             (and (exists? js/window) js/window.goog.DEBUG))
+             (and (exists? js/goog) js/goog.DEBUG))
      (assert-no-entities tx-data))
    (let [tx-data (map (fn [m]
                         (if (map? m)

+ 73 - 72
deps/db/src/logseq/db/frontend/class.cljs

@@ -2,6 +2,7 @@
   "Class related fns for DB graphs and frontend/datascript usage"
   (:require [clojure.set :as set]
             [flatland.ordered.map :refer [ordered-map]]
+            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.db.frontend.db-ident :as db-ident]
             [logseq.db.sqlite.util :as sqlite-util]))
 
@@ -10,76 +11,77 @@
 
 (def ^:large-vars/data-var built-in-classes
   "Map of built-in classes for db graphs with their :db/ident as keys"
-  (ordered-map
-   :logseq.class/Root {:title "Root Tag"}
-
-   :logseq.class/Tag {:title "Tag"}
-
-   :logseq.class/Property {:title "Property"}
-
-   :logseq.class/Page {:title "Page"}
-
-   :logseq.class/Journal
-   {:title "Journal"
-    :properties {:logseq.property/parent :logseq.class/Page
-                 :logseq.property.journal/title-format "MMM do, yyyy"}}
-
-   :logseq.class/Whiteboard
-   {:title "Whiteboard"
-    :properties {:logseq.property/parent :logseq.class/Page}}
-
-   :logseq.class/Task
-   {:title "Task"
-    :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}}
-
-   :logseq.class/Query
-   {:title "Query"
-    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}}
-    :schema {:properties [:logseq.property/query]}}
-
-   :logseq.class/Card
-   {:title "Card"
-    :schema {:properties [:logseq.property.fsrs/state :logseq.property.fsrs/due]}}
-
-   :logseq.class/Cards
-   {:title "Cards"
-    :properties {:logseq.property/icon {:type :tabler-icon :id "search"}
-                 :logseq.property/parent :logseq.class/Query}}
-
-   :logseq.class/Asset
-   {:title "Asset"
-    :properties {;; :logseq.property/icon {:type :tabler-icon :id "file"}
-                 :logseq.property.class/hide-from-node true
-                 :logseq.property.view/type :logseq.property.view/type.gallery}
-    :schema {:properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]
-             :required-properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]}}
-
-   :logseq.class/Code-block
-   {:title "Code"
-    :properties {:logseq.property.class/hide-from-node true}
-    :schema {:properties [:logseq.property.node/display-type :logseq.property.code/lang]}}
-
-   :logseq.class/Quote-block
-   {:title "Quote"
-    :properties {:logseq.property.class/hide-from-node true}
-    :schema {:properties [:logseq.property.node/display-type]}}
-
-   :logseq.class/Math-block
-   {:title "Math"
-    :properties {:logseq.property.class/hide-from-node true}
-    :schema {:properties [:logseq.property.node/display-type]}}
-
-   :logseq.class/Pdf-annotation
-   {:title "PDF Annotation"
-    :properties {:logseq.property.class/hide-from-node true}
-    :schema {:properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
-                          :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value
-                          :logseq.property.pdf/hl-type :logseq.property.pdf/hl-image]
-             :required-properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
-                                   :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value]}}
-
-;; TODO: Add more classes such as :book, :paper, :movie, :music, :project)
-   ))
+  (apply
+   ordered-map
+   (defkeywords
+     :logseq.class/Root {:title "Root Tag"}
+
+     :logseq.class/Tag {:title "Tag"}
+
+     :logseq.class/Property {:title "Property"}
+
+     :logseq.class/Page {:title "Page"}
+
+     :logseq.class/Journal
+     {:title "Journal"
+      :properties {:logseq.property/parent :logseq.class/Page
+                   :logseq.property.journal/title-format "MMM do, yyyy"}}
+
+     :logseq.class/Whiteboard
+     {:title "Whiteboard"
+      :properties {:logseq.property/parent :logseq.class/Page}}
+
+     :logseq.class/Task
+     {:title "Task"
+      :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline :logseq.task/scheduled]}}
+
+     :logseq.class/Query
+     {:title "Query"
+      :properties {:logseq.property/icon {:type :tabler-icon :id "search"}}
+      :schema {:properties [:logseq.property/query]}}
+
+     :logseq.class/Card
+     {:title "Card"
+      :schema {:properties [:logseq.property.fsrs/state :logseq.property.fsrs/due]}}
+
+     :logseq.class/Cards
+     {:title "Cards"
+      :properties {:logseq.property/icon {:type :tabler-icon :id "search"}
+                   :logseq.property/parent :logseq.class/Query}}
+
+     :logseq.class/Asset
+     {:title "Asset"
+      :properties {;; :logseq.property/icon {:type :tabler-icon :id "file"}
+                   :logseq.property.class/hide-from-node true
+                   :logseq.property.view/type :logseq.property.view/type.gallery}
+      :schema {:properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]
+               :required-properties [:logseq.property.asset/type :logseq.property.asset/size :logseq.property.asset/checksum]}}
+
+     :logseq.class/Code-block
+     {:title "Code"
+      :properties {:logseq.property.class/hide-from-node true}
+      :schema {:properties [:logseq.property.node/display-type :logseq.property.code/lang]}}
+
+     :logseq.class/Quote-block
+     {:title "Quote"
+      :properties {:logseq.property.class/hide-from-node true}
+      :schema {:properties [:logseq.property.node/display-type]}}
+
+     :logseq.class/Math-block
+     {:title "Math"
+      :properties {:logseq.property.class/hide-from-node true}
+      :schema {:properties [:logseq.property.node/display-type]}}
+
+     :logseq.class/Pdf-annotation
+     {:title "PDF Annotation"
+      :properties {:logseq.property.class/hide-from-node true}
+      :schema {:properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
+                            :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value
+                            :logseq.property.pdf/hl-type :logseq.property.pdf/hl-image]
+               :required-properties [:logseq.property/ls-type :logseq.property.pdf/hl-color :logseq.property/asset
+                                     :logseq.property.pdf/hl-page :logseq.property.pdf/hl-value]}}
+     ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project)
+     )))
 
 (def page-children-classes
   "Children of :logseq.class/Page"
@@ -123,6 +125,5 @@
   [db page-m]
   {:pre [(string? (:block/title page-m))]}
   (let [db-ident (create-user-class-ident-from-name (:block/title page-m))
-        db-ident' (or (:db/ident page-m)
-                      (db-ident/ensure-unique-db-ident db db-ident))]
+        db-ident' (db-ident/ensure-unique-db-ident db db-ident)]
     (sqlite-util/build-new-class (assoc page-m :db/ident db-ident'))))

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

@@ -24,8 +24,7 @@
 
 (defn class?
   [entity]
-  (or (has-tag? entity :logseq.class/Tag)
-      (keyword-identical? (:db/ident entity) :logseq.class/Tag)))
+  (has-tag? entity :logseq.class/Tag))
 
 (defn property?
   [entity]

+ 24 - 0
deps/db/src/logseq/db/frontend/kv_entity.cljs

@@ -0,0 +1,24 @@
+(ns logseq.db.frontend.kv-entity
+  "kv entities used by logseq db"
+  (:require [logseq.common.defkeywords :refer [defkeywords]]))
+
+(defkeywords
+  :logseq.kv/db-type                      {:doc "Set to \"db\" if it's a db-graph"}
+  :logseq.kv/graph-uuid                   {:doc "Store graph-uuid if it's a rtc enabled graph"
+                                           :rtc {:rtc/ignore-entity-when-init-upload true
+                                                 :rtc/ignore-entity-when-init-download true}}
+  :logseq.kv/import-type                  {:doc "If graph is imported, identifies how a graph is imported including which UI or CLI import process. CLI scripts can set this to a custom value.
+                                                 UI values include :file-graph and :sqlite-db and CLI values start with :cli e.g. :cli/default."}
+  :logseq.kv/imported-at                  {:doc "Time if graph is imported"}
+  :logseq.kv/graph-local-tx               {:doc "local rtc tx-id"
+                                           :rtc {:rtc/ignore-entity-when-init-upload true
+                                                 :rtc/ignore-entity-when-init-download true}}
+  :logseq.kv/schema-version               {:doc "Graph's current schema version"}
+  :logseq.kv/graph-created-at             {:doc "Graph's created at time"}
+  :logseq.kv/latest-code-lang             {:doc "Latest lang used by a #Code-block"
+                                           :rtc {:rtc/ignore-entity-when-init-upload true
+                                                 :rtc/ignore-entity-when-init-download true}}
+  :logseq.kv/graph-backup-folder          {:doc "Backup folder for automated backup feature"
+                                           :rtc {:rtc/ignore-entity-when-init-upload true
+                                                 :rtc/ignore-entity-when-init-download true}}
+  :logseq.kv/graph-initial-schema-version {:doc "Graph's schema version when created"})

+ 59 - 34
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -111,7 +111,8 @@
    of validate-property-value"
   (set/union
    (set (get-in db-class/built-in-classes [:logseq.class/Asset :schema :required-properties]))
-   #{:logseq.property/created-from-property}))
+   #{:logseq.property/created-from-property :logseq.property.history/scalar-value
+     :logseq.property.history/block :logseq.property.history/property :logseq.property.history/ref-value}))
 
 (defn- property-entity->map
   "Provide the minimal number of property attributes to validate the property
@@ -191,7 +192,7 @@
 (defn datoms->entities
   "Returns a vec of entity maps given :eavt datoms"
   [datoms]
-  (mapv (fn [[db-id m]] (with-meta m {:db/id db-id}))
+  (mapv (fn [[db-id m]] (assoc m :db/id db-id))
         (datoms->entity-maps datoms)))
 
 (assert (every? #(re-find #"^(block|logseq\.)" (namespace %)) db-property/db-attribute-properties)
@@ -366,13 +367,26 @@
     (remove #(#{:block/title :logseq.property/created-from-property} (first %)) block-attrs)
     page-or-block-attrs)))
 
-(def property-history-block
+(def property-history-block*
   [:map
    [:block/uuid :uuid]
    [:block/created-at :int]
-   [:block/properties block-properties]
+   [:block/updated-at {:optional true} :int]
+   [:logseq.property.history/block :int]
+   [:logseq.property.history/property :int]
+   [:logseq.property.history/ref-value {:optional true} :int]
+   [:logseq.property.history/scalar-value {:optional true} :any]
    [:block/tx-id {:optional true} :int]])
 
+(def property-history-block
+  "A closed value for a property with closed/allowed values"
+  [:and property-history-block*
+   [:fn {:error/message ":logseq.property.history/ref-value or :logseq.property.history/scalar-value required"
+         :error/path [:logseq.property.history/ref-value]}
+    (fn [m]
+      (or (:logseq.property.history/ref-value m)
+          (some? (:logseq.property.history/scalar-value m))))]])
+
 (def closed-value-block*
   (vec
    (concat
@@ -406,10 +420,7 @@
   "A block has content and a page"
   [:or
    normal-block
-   closed-value-block
-   whiteboard-block
-   property-value-block
-   property-history-block])
+   whiteboard-block])
 
 (def asset-block
   "A block tagged with #Asset"
@@ -448,38 +459,52 @@
    [:db/ident [:= :logseq.property/empty-placeholder]]
    [:block/tx-id {:optional true} :int]])
 
+(defn entity-dispatch-key [db ent]
+  (let [d (if (:block/uuid ent) (d/entity db [:block/uuid (:block/uuid ent)]) ent)
+        ;; order matters as some block types are a subset of others e.g. :whiteboard
+        dispatch-key (cond
+                       (entity-util/property? d)
+                       :property
+                       (entity-util/class? d)
+                       :class
+                       (entity-util/hidden? d)
+                       :hidden
+                       (entity-util/whiteboard? d)
+                       :normal-page
+                       (entity-util/page? d)
+                       :normal-page
+                       (entity-util/asset? d)
+                       :asset-block
+                       (:file/path d)
+                       :file-block
+                       (:logseq.property.history/block d)
+                       :property-history-block
+
+                       (:block/closed-value-property d)
+                       :closed-value-block
+
+                       (and (:logseq.property/created-from-property d)
+                            (:property.value/content d))
+                       :property-value-block
+
+                       (:block/uuid d)
+                       :block
+                       (= (:db/ident d) :logseq.property/empty-placeholder)
+                       :property-value-placeholder
+                       (:db/ident d)
+                       :db-ident-key-value)]
+    dispatch-key))
+
 (def Data
   (into
-   [:multi {:dispatch (fn [d]
-                        ;; order matters as some block types are a subset of others e.g. :whiteboard
-                        (let [db *db-for-validate-fns*
-                              d (if (:block/uuid d) (d/entity db [:block/uuid (:block/uuid d)]) d)
-                              dispatch-key (cond
-                                             (entity-util/property? d)
-                                             :property
-                                             (entity-util/class? d)
-                                             :class
-                                             (entity-util/hidden? (:block/title d))
-                                             :hidden
-                                             (entity-util/whiteboard? d)
-                                             :normal-page
-                                             (entity-util/page? d)
-                                             :normal-page
-                                             (entity-util/asset? d)
-                                             :asset-block
-                                             (:file/path d)
-                                             :file-block
-                                             (:block/uuid d)
-                                             :block
-                                             (= (:db/ident d) :logseq.property/empty-placeholder)
-                                             :property-value-placeholder
-                                             (:db/ident d)
-                                             :db-ident-key-value)]
-                          dispatch-key))}]
+   [:multi {:dispatch (fn [d] (entity-dispatch-key *db-for-validate-fns* d))}]
    {:property property-page
     :class class-page
     :hidden hidden-page
     :normal-page normal-page
+    :property-history-block property-history-block
+    :closed-value-block closed-value-block
+    :property-value-block property-value-block
     :block block
     :asset-block asset-block
     :file-block file-block

+ 473 - 451
deps/db/src/logseq/db/frontend/property.cljs

@@ -4,6 +4,7 @@
             [clojure.string :as string]
             [datascript.core :as d]
             [flatland.ordered.map :refer [ordered-map]]
+            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]
             [logseq.db.frontend.db-ident :as db-ident]
@@ -51,232 +52,223 @@
    * :attribute - Property keyword that is saved to a datascript attribute outside of :block/properties
    * :queryable? - Boolean for whether property can be queried in the query builder
    * :closed-values - Vec of closed-value maps for properties with choices. Map
-     has keys :value, :db-ident, :uuid and :icon"
-  (ordered-map
-   :property/type {:title "Property type"
-                   :schema {:type :keyword
-                            :hide? true}}
-   :property/hide? {:title "Hide this property"
-                    :schema {:type :checkbox
-                             :hide? true}}
-   :property/public? {:title "Property public?"
-                      :schema {:type :checkbox
-                               :hide? true}}
-   :property/view-context {:title "Property view context"
-                           :schema {:type :keyword
-                                    :hide? true}}
-   :property/ui-position {:title "Property position"
-                          :schema {:type :keyword
-                                   :hide? true}}
-   :property/schema.classes
-   {:title "Property classes"
-    :schema {:type :entity
-             :cardinality :many
-             :public? false
-             :hide? true}}
-   :property.value/content
-   {:title "Property value"
-    :schema {:type :any
-             :public? false
-             :hide? true}}
-   :block/alias           {:title "Alias"
-                           :attribute :block/alias
-                           :schema {:type :page
-                                    :cardinality :many
-                                    :view-context :page
-                                    :public? true}
-                           :queryable? true}
-   :block/tags           {:title "Tags"
-                          :attribute :block/tags
-                          :schema {:type :class
-                                   :cardinality :many
-                                   :public? true
-                                   :classes #{:logseq.class/Root}}
-                          :queryable? true}
-   :logseq.property.attribute/kv-value {:title "KV value"
-                                        :attribute :kv/value
-                                        :schema {:type :any
-                                                 :public? false
-                                                 :hide? true}}
-   :block/parent         {:title "Node parent"
-                          :attribute :block/parent
-                          :schema {:type :entity
-                                   :public? false
-                                   :hide? true}}
-   :block/order          {:title "Node order"
-                          :attribute :block/order
-                          :schema {:type :string
-                                   :public? false
-                                   :hide? true}}
-   :block/collapsed?     {:title "Node collapsed?"
-                          :attribute :block/collapsed?
-                          :schema {:type :checkbox
-                                   :public? false
-                                   :hide? true}}
-   :block/page           {:title "Node page"
-                          :attribute :block/page
-                          :schema {:type :entity
-                                   :public? false
-                                   :hide? true}}
-   :block/refs           {:title "Node references"
-                          :attribute :block/refs
-                          :schema {:type :entity
-                                   :cardinality :many
-                                   :public? false
-                                   :hide? true}}
-   :block/path-refs      {:title "Node path references"
-                          :attribute :block/path-refs
-                          :schema {:type :entity
-                                   :cardinality :many
-                                   :public? false
-                                   :hide? true}}
-   :block/link           {:title "Node links to"
-                          :attribute :block/link
-                          :schema {:type :entity
-                                   :public? false
-                                   :hide? true}}
-   :block/title          {:title "Node title"
-                          :attribute :block/title
-                          :schema {:type :string
-                                   :public? false
-                                   :hide? true}}
-   :block/closed-value-property  {:title "Closed value property"
-                                  :attribute :block/closed-value-property
-                                  :schema {:type :entity
-                                           :public? false
-                                           :hide? true}}
-   :block/created-at     {:title "Node created at"
-                          :attribute :block/created-at
-                          :schema {:type :datetime
-                                   :public? false
-                                   :hide? true}}
-   :block/updated-at     {:title "Node updated at"
-                          :attribute :block/updated-at
-                          :schema {:type :datetime
-                                   :public? false
-                                   :hide? true}}
-
-   :logseq.property.node/display-type {:title "Node Display Type"
-                                       :schema {:type :keyword
-                                                :public? false
-                                                :hide? true
-                                                :view-context :block}
-                                       :queryable? true}
-   :logseq.property.code/lang {:title "Code Mode"
-                               :schema {:type :string
-                                        :public? false
-                                        :hide? true
-                                        :view-context :block}
-                               :queryable? true}
-   :logseq.property/parent {:title "Parent"
-                            :schema {:type :node
+     has keys :value, :db-ident, :uuid and :icon
+   * :rtc - submap for RTC configs. view docs by jumping to keyword definitions.
+  "
+  (apply
+   ordered-map
+   (defkeywords
+     :block/alias           {:title "Alias"
+                             :attribute :block/alias
+                             :schema {:type :page
+                                      :cardinality :many
+                                      :view-context :page
+                                      :public? true}
+                             :queryable? true}
+     :block/tags           {:title "Tags"
+                            :attribute :block/tags
+                            :schema {:type :class
+                                     :cardinality :many
                                      :public? true
-                                     :view-context :page}
+                                     :classes #{:logseq.class/Root}}
                             :queryable? true}
-   :logseq.property/default-value {:title "Default value"
-                                   :schema {:type :entity
-                                            :public? false
-                                            :hide? true
-                                            :view-context :property}}
-   :logseq.property/scalar-default-value {:title "Non ref type default value"
+     :logseq.property.attribute/kv-value {:title "KV value"
+                                          :attribute :kv/value
                                           :schema {:type :any
                                                    :public? false
-                                                   :hide? true
-                                                   :view-context :property}}
-   :logseq.property.class/properties {:title "Tag Properties"
-                                      :schema {:type :property
-                                               :cardinality :many
-                                               :public? true
-                                               :view-context :never}}
-   :logseq.property/hide-empty-value {:title "Hide empty value"
-                                      :schema {:type :checkbox
-                                               :public? true
-                                               :view-context :property}}
-   :logseq.property.class/hide-from-node {:title "Hide from Node"
-                                          :schema {:type :checkbox
-                                                   :public? true
-                                                   :view-context :class}}
-   :logseq.property/query       {:title "Query"
-                                 :schema {:type :default
-                                          :public? true
+                                                   :hide? true}}
+     :block/parent         {:title "Node parent"
+                            :attribute :block/parent
+                            :schema {:type :entity
+                                     :public? false
+                                     :hide? true}}
+     :block/order          {:title "Node order"
+                            :attribute :block/order
+                            :schema {:type :string
+                                     :public? false
+                                     :hide? true}}
+     :block/collapsed?     {:title "Node collapsed?"
+                            :attribute :block/collapsed?
+                            :schema {:type :checkbox
+                                     :public? false
+                                     :hide? true}}
+     :block/page           {:title "Node page"
+                            :attribute :block/page
+                            :schema {:type :entity
+                                     :public? false
+                                     :hide? true}}
+     :block/refs           {:title "Node references"
+                            :attribute :block/refs
+                            :schema {:type :entity
+                                     :cardinality :many
+                                     :public? false
+                                     :hide? true}}
+     :block/path-refs      {:title "Node path references"
+                            :attribute :block/path-refs
+                            :schema {:type :entity
+                                     :cardinality :many
+                                     :public? false
+                                     :hide? true}}
+     :block/link           {:title "Node links to"
+                            :attribute :block/link
+                            :schema {:type :entity
+                                     :public? false
+                                     :hide? true}}
+     :block/title          {:title "Node title"
+                            :attribute :block/title
+                            :schema {:type :string
+                                     :public? false
+                                     :hide? true}}
+     :block/closed-value-property  {:title "Closed value property"
+                                    :attribute :block/closed-value-property
+                                    :schema {:type :entity
+                                             :public? false
+                                             :hide? true}}
+     :block/created-at     {:title "Node created at"
+                            :attribute :block/created-at
+                            :schema {:type :datetime
+                                     :public? false
+                                     :hide? true}}
+     :block/updated-at     {:title "Node updated at"
+                            :attribute :block/updated-at
+                            :schema {:type :datetime
+                                     :public? false
+                                     :hide? true}}
+     :logseq.property.attribute/property-schema-classes
+     {:title "Property classes"
+      :attribute :property/schema.classes
+      :schema {:type :entity
+               :cardinality :many
+               :public? false
+               :hide? true}}
+     :logseq.property.attribute/property-value-content
+     {:title "Property value"
+      :attribute :property.value/content
+      :schema {:type :any
+               :public? false
+               :hide? true}}
+
+     :logseq.property.node/display-type {:title "Node Display Type"
+                                         :schema {:type :keyword
+                                                  :public? false
+                                                  :hide? true
+                                                  :view-context :block}
+                                         :queryable? true}
+     :logseq.property.code/lang {:title "Code Mode"
+                                 :schema {:type :string
+                                          :public? false
                                           :hide? true
-                                          :view-context :block}}
-   :logseq.property/page-tags {:title "Page Tags"
-                               :schema {:type :page
-                                        :public? true
-                                        :view-context :page
-                                        :cardinality :many}}
-   :logseq.property/background-color {:title "Background color"
-                                      :schema {:type :default :hide? true}}
-   :logseq.property/background-image {:title "Background image"
-                                      :schema
-                                      {:type :default ; FIXME: asset
-                                       :view-context :block}}
+                                          :view-context :block}
+                                 :queryable? true}
+     :logseq.property/parent {:title "Parent"
+                              :schema {:type :node
+                                       :public? true
+                                       :view-context :page}
+                              :queryable? true}
+     :logseq.property/default-value {:title "Default value"
+                                     :schema {:type :entity
+                                              :public? false
+                                              :hide? true
+                                              :view-context :property}}
+     :logseq.property/scalar-default-value {:title "Non ref type default value"
+                                            :schema {:type :any
+                                                     :public? false
+                                                     :hide? true
+                                                     :view-context :property}}
+     :logseq.property.class/properties {:title "Tag Properties"
+                                        :schema {:type :property
+                                                 :cardinality :many
+                                                 :public? true
+                                                 :view-context :never}}
+     :logseq.property/hide-empty-value {:title "Hide empty value"
+                                        :schema {:type :checkbox
+                                                 :public? true
+                                                 :view-context :property}}
+     :logseq.property.class/hide-from-node {:title "Hide from Node"
+                                            :schema {:type :checkbox
+                                                     :public? true
+                                                     :view-context :class}}
+     :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
+                                          :view-context :page
+                                          :cardinality :many}}
+     :logseq.property/background-color {:title "Background color"
+                                        :schema {:type :default :hide? true}}
+     :logseq.property/background-image {:title "Background image"
+                                        :schema
+                                        {:type :default ; FIXME: asset
+                                         :view-context :block}}
    ;; number (1-6) or boolean for auto heading
-   :logseq.property/heading {:title "Heading"
-                             :schema {:type :any :hide? true}
-                             :queryable? true}
-   :logseq.property/created-from-property {:title "Created from property"
-                                           :schema {:type :entity
-                                                    :hide? true}}
-   :logseq.property/built-in?             {:title "Built in?"
-                                           :schema {:type :checkbox
-                                                    :hide? true}}
-   :logseq.property/asset   {:title "Asset"
-                             :schema {:type :entity
-                                      :hide? true}}
+     :logseq.property/heading {:title "Heading"
+                               :schema {:type :any :hide? true}
+                               :queryable? true}
+     :logseq.property/created-from-property {:title "Created from property"
+                                             :schema {:type :entity
+                                                      :hide? true}}
+     :logseq.property/built-in?             {:title "Built in?"
+                                             :schema {:type :checkbox
+                                                      :hide? true}}
+     :logseq.property/asset   {:title "Asset"
+                               :schema {:type :entity
+                                        :hide? true}}
    ;; used by pdf and whiteboard
    ;; TODO: remove ls-type
-   :logseq.property/ls-type {:schema {:type :keyword
-                                      :hide? true}}
-
-   :logseq.property.pdf/hl-type {:title "Annotation type"
-                                 :schema {:type :keyword :hide? true}}
-   :logseq.property.pdf/hl-color
-   {:title "Annotation color"
-    :schema {:type :default :hide? true}
-    :closed-values
-    (mapv (fn [[db-ident value]]
-            {:db-ident db-ident
-             :value value
-             :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
-          [[:logseq.property/color.yellow "yellow"]
-           [:logseq.property/color.red "red"]
-           [:logseq.property/color.green "green"]
-           [:logseq.property/color.blue "blue"]
-           [:logseq.property/color.purple "purple"]])}
-   :logseq.property.pdf/hl-page {:title "Annotation page"
-                                 :schema {:type :raw-number :hide? true}}
-   :logseq.property.pdf/hl-image {:title "Annotation image"
-                                  :schema {:type :entity :hide? true}}
-   :logseq.property.pdf/hl-value {:title "Annotation data"
-                                  :schema {:type :map :hide? true}}
+     :logseq.property/ls-type {:schema {:type :keyword
+                                        :hide? true}}
+
+     :logseq.property.pdf/hl-type {:title "Annotation type"
+                                   :schema {:type :keyword :hide? true}}
+     :logseq.property.pdf/hl-color
+     {:title "Annotation color"
+      :schema {:type :default :hide? true}
+      :closed-values
+      (mapv (fn [[db-ident value]]
+              {:db-ident db-ident
+               :value value
+               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
+            [[:logseq.property/color.yellow "yellow"]
+             [:logseq.property/color.red "red"]
+             [:logseq.property/color.green "green"]
+             [:logseq.property/color.blue "blue"]
+             [:logseq.property/color.purple "purple"]])}
+     :logseq.property.pdf/hl-page {:title "Annotation page"
+                                   :schema {:type :raw-number :hide? true}}
+     :logseq.property.pdf/hl-image {:title "Annotation image"
+                                    :schema {:type :entity :hide? true}}
+     :logseq.property.pdf/hl-value {:title "Annotation data"
+                                    :schema {:type :map :hide? true}}
    ;; FIXME: :logseq.property/order-list-type should updated to closed values
-   :logseq.property/order-list-type {:title "List type"
-                                     :name :logseq.order-list-type
-                                     :schema {:type :default
-                                              :hide? true}}
-   :logseq.property.linked-references/includes {:title "Included references"
-                                                :schema {; could be :entity to support blocks(objects) in the future
-                                                         :type :node
-                                                         :cardinality :many
-                                                         :hide? true}}
-   :logseq.property.linked-references/excludes {:title "Excluded references"
-                                                :schema {:type :node
-                                                         :cardinality :many
-                                                         :hide? true}}
-   :logseq.property.tldraw/page {:name :logseq.tldraw.page
-                                 :schema {:type :map
-                                          :hide? true}}
-   :logseq.property.tldraw/shape {:name :logseq.tldraw.shape
-                                  :schema {:type :map
-                                           :hide? true}}
+     :logseq.property/order-list-type {:title "List type"
+                                       :name :logseq.order-list-type
+                                       :schema {:type :default
+                                                :hide? true}}
+     :logseq.property.linked-references/includes {:title "Included references"
+                                                  :schema {; could be :entity to support blocks(objects) in the future
+                                                           :type :node
+                                                           :cardinality :many
+                                                           :hide? true}}
+     :logseq.property.linked-references/excludes {:title "Excluded references"
+                                                  :schema {:type :node
+                                                           :cardinality :many
+                                                           :hide? true}}
+     :logseq.property.tldraw/page {:name :logseq.tldraw.page
+                                   :schema {:type :map
+                                            :hide? true}}
+     :logseq.property.tldraw/shape {:name :logseq.tldraw.shape
+                                    :schema {:type :map
+                                             :hide? true}}
 
    ;; Journal props
-   :logseq.property.journal/title-format {:title "Title Format"
-                                          :schema
-                                          {:type :string
-                                           :public? false}}
+     :logseq.property.journal/title-format {:title "Title Format"
+                                            :schema
+                                            {:type :string
+                                             :public? false}}
 
    ;; TODO: should we replace block/journal-day with those separate props?
    ;; :logseq.property.journal/year {:title "Journal year"
@@ -292,260 +284,290 @@
    ;;                               {:type :raw-number
    ;;                                :public? false}}
 
-   :logseq.property/choice-checkbox-state
-   {:title "Choice checkbox state"
-    :schema {:type :checkbox
-             :hide? true}
-    :queryable? false}
-   :logseq.property/checkbox-display-properties
-   {:title "Properties displayed as checkbox"
-    :schema {:type :property
-             :cardinality :many
-             :hide? true}
-    :queryable? false}
+     :logseq.property/choice-checkbox-state
+     {:title "Choice checkbox state"
+      :schema {:type :checkbox
+               :hide? true}
+      :queryable? false}
+     :logseq.property/checkbox-display-properties
+     {:title "Properties displayed as checkbox"
+      :schema {:type :property
+               :cardinality :many
+               :hide? true}
+      :queryable? false}
    ;; Task props
-   :logseq.task/priority
-   {:title "Priority"
-    :schema
-    {:type :default
-     :public? true
-     :ui-position :block-left}
-    :closed-values
-    (mapv (fn [[db-ident value icon]]
-            {:db-ident db-ident
-             :value value
-             :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
-             :icon {:type :tabler-icon :id icon}})
-          [[:logseq.task/priority.low "Low" "priorityLvlLow"]
-           [:logseq.task/priority.medium "Medium" "priorityLvlMedium"]
-           [:logseq.task/priority.high "High" "priorityLvlHigh"]
-           [:logseq.task/priority.urgent "Urgent" "priorityLvlUrgent"]])
-    :properties {:logseq.property/hide-empty-value true
-                 :logseq.property/enable-history? true}}
-   :logseq.task/status
-   {:title "Status"
-    :schema
-    {:type :default
-     :public? true
-     :ui-position :block-left}
-    :closed-values
-    (mapv (fn [[db-ident value icon checkbox-state]]
-            {:db-ident db-ident
-             :value value
-             :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
-             :icon {:type :tabler-icon :id icon}
-             :properties (when (some? checkbox-state)
-                           {:logseq.property/choice-checkbox-state checkbox-state})})
-          [[:logseq.task/status.backlog "Backlog" "Backlog"]
-           [:logseq.task/status.todo "Todo" "Todo" false]
-           [:logseq.task/status.doing "Doing" "InProgress50"]
-           [:logseq.task/status.in-review "In Review" "InReview"]
-           [:logseq.task/status.done "Done" "Done" true]
-           [:logseq.task/status.canceled "Canceled" "Cancelled"]])
-    :properties {:logseq.property/hide-empty-value true
-                 :logseq.property/default-value :logseq.task/status.todo
-                 :logseq.property/enable-history? true}
-    :queryable? true}
-   :logseq.task/deadline
-   {:title "Deadline"
-    :schema {:type :datetime
-             :public? true
-             :ui-position :block-below}
-    :properties {:logseq.property/hide-empty-value true}
-    :queryable? true}
-   :logseq.task/scheduled
-   {:title "Scheduled"
-    :schema {:type :datetime
-             :public? true
-             :ui-position :block-below}
-    :properties {:logseq.property/hide-empty-value true}
-    :queryable? true}
-   :logseq.task/recur-frequency
-   (let [schema {:type :number
-                 :public? false}]
-     {:title "Recur frequency"
-      :schema schema
-      :properties (let [block {:db/ident :logseq.task/recur-frequency
-                               :property/type :number}
-                        property {:db/ident :logseq.property/default-value
-                                  :property/type :entity}
-                        default-value (assoc (build-property-value-block block property 1) :db/id -1)]
-                    {:logseq.property/hide-empty-value true
-                     :logseq.property/default-value default-value})
-      :queryable? true})
-   :logseq.task/recur-unit
-   {:title "Recur unit"
-    :schema {:type :default
-             :public? false}
-    :closed-values (mapv (fn [[db-ident value]]
-                           {:db-ident db-ident
-                            :value value
-                            :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
-                         [[:logseq.task/recur-unit.minute "Minute"]
-                          [:logseq.task/recur-unit.hour "Hour"]
-                          [:logseq.task/recur-unit.day "Day"]
-                          [:logseq.task/recur-unit.week "Week"]
-                          [:logseq.task/recur-unit.month "Month"]
-                          [:logseq.task/recur-unit.year "Year"]])
-    :properties {:logseq.property/hide-empty-value true
-                 :logseq.property/default-value :logseq.task/recur-unit.day}
-    :queryable? true}
-   :logseq.task/repeated?
-   {:title "Repeated task?"
-    :schema {:type :checkbox
-             :hide? true}
-    :queryable? true}
-   :logseq.task/scheduled-on-property
-   {:title "Scheduled on property"
-    :schema {:type :property
-             :hide? true}}
-   :logseq.task/recur-status-property
-   {:title "Recur status property"
-    :schema {:type :property
-             :hide? true}}
+     :logseq.task/priority
+     {:title "Priority"
+      :schema
+      {:type :default
+       :public? true
+       :position :block-left}
+      :closed-values
+      (mapv (fn [[db-ident value icon]]
+              {:db-ident db-ident
+               :value value
+               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
+               :icon {:type :tabler-icon :id icon}})
+            [[:logseq.task/priority.low "Low" "priorityLvlLow"]
+             [:logseq.task/priority.medium "Medium" "priorityLvlMedium"]
+             [:logseq.task/priority.high "High" "priorityLvlHigh"]
+             [:logseq.task/priority.urgent "Urgent" "priorityLvlUrgent"]])
+      :properties {:logseq.property/hide-empty-value true
+                   :logseq.property/enable-history? true}}
+     :logseq.task/status
+     {:title "Status"
+      :schema
+      {:type :default
+       :public? true
+       :position :block-left}
+      :closed-values
+      (mapv (fn [[db-ident value icon checkbox-state]]
+              {:db-ident db-ident
+               :value value
+               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
+               :icon {:type :tabler-icon :id icon}
+               :properties (when (some? checkbox-state)
+                             {:logseq.property/choice-checkbox-state checkbox-state})})
+            [[:logseq.task/status.backlog "Backlog" "Backlog"]
+             [:logseq.task/status.todo "Todo" "Todo" false]
+             [:logseq.task/status.doing "Doing" "InProgress50"]
+             [:logseq.task/status.in-review "In Review" "InReview"]
+             [:logseq.task/status.done "Done" "Done" true]
+             [:logseq.task/status.canceled "Canceled" "Cancelled"]])
+      :properties {:logseq.property/hide-empty-value true
+                   :logseq.property/default-value :logseq.task/status.todo
+                   :logseq.property/enable-history? true}
+      :queryable? true}
+     :logseq.task/deadline
+     {:title "Deadline"
+      :schema {:type :datetime
+               :public? true
+               :position :block-below}
+      :properties {:logseq.property/hide-empty-value true}
+      :queryable? true}
+     :logseq.task/scheduled
+     {:title "Scheduled"
+      :schema {:type :datetime
+               :public? true
+               :position :block-below}
+      :properties {:logseq.property/hide-empty-value true}
+      :queryable? true}
+     :logseq.task/recur-frequency
+     (let [schema {:type :number
+                   :public? false}]
+       {:title "Recur frequency"
+        :schema schema
+        :properties (let [block {:db/ident :logseq.task/recur-frequency
+                                 :property/type :number}
+                          property {:db/ident :logseq.property/default-value
+                                    :property/type :entity}
+                          default-value (assoc (build-property-value-block block property 1) :db/id -1)]
+                      {:logseq.property/hide-empty-value true
+                       :logseq.property/default-value default-value})
+        :queryable? true})
+     :logseq.task/recur-unit
+     {:title "Recur unit"
+      :schema {:type :default
+               :public? false}
+      :closed-values (mapv (fn [[db-ident value]]
+                             {:db-ident db-ident
+                              :value value
+                              :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
+                           [[:logseq.task/recur-unit.minute "Minute"]
+                            [:logseq.task/recur-unit.hour "Hour"]
+                            [:logseq.task/recur-unit.day "Day"]
+                            [:logseq.task/recur-unit.week "Week"]
+                            [:logseq.task/recur-unit.month "Month"]
+                            [:logseq.task/recur-unit.year "Year"]])
+      :properties {:logseq.property/hide-empty-value true
+                   :logseq.property/default-value :logseq.task/recur-unit.day}
+      :queryable? true}
+     :logseq.task/repeated?
+     {:title "Repeated task?"
+      :schema {:type :checkbox
+               :hide? true}
+      :queryable? true}
+     :logseq.task/scheduled-on-property
+     {:title "Scheduled on property"
+      :schema {:type :property
+               :hide? true}}
+     :logseq.task/recur-status-property
+     {:title "Recur status property"
+      :schema {:type :property
+               :hide? true}}
 
 ;; TODO: Add more props :Assignee, :Estimate, :Cycle, :Project
 
-   :logseq.property/icon {:title "Icon"
-                          :schema {:type :map}}
-   :logseq.property/public {:title "Publishing Public?"
-                            :schema
-                            {:type :checkbox
-                             :hide? true
-                             :view-context :page
-                             :public? true}}
-   :logseq.property/exclude-from-graph-view {:title "Excluded from Graph view?"
-                                             :schema
-                                             {:type :checkbox
-                                              :hide? true
-                                              :view-context :page
-                                              :public? true}}
-   :logseq.property/description {:title "Description"
-                                 :schema
-                                 {:type :default
-                                  :public? true}}
-
-   :logseq.property.view/type
-   {:title "View Type"
-    :schema
-    {:type :default
-     :public? false
-     :hide? true}
-    :closed-values
-    (mapv (fn [[db-ident value]]
-            {:db-ident db-ident
-             :value value
-             :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
-          [[:logseq.property.view/type.table "Table View"]
-           [:logseq.property.view/type.list "List View"]
-           [:logseq.property.view/type.gallery "Gallery View"]])
-    :properties {:logseq.property/default-value :logseq.property.view/type.table}
-    :queryable? true}
-
-   :logseq.property.table/sorting {:title "View sorting"
-                                   :schema
-                                   {:type :coll
-                                    :hide? true
-                                    :public? false}}
-
-   :logseq.property.table/filters {:title "View filters"
-                                   :schema
-                                   {:type :coll
-                                    :hide? true
-                                    :public? false}}
-
-   :logseq.property.table/hidden-columns {:title "View hidden columns"
-                                          :schema
-                                          {:type :keyword
-                                           :cardinality :many
-                                           :hide? true
-                                           :public? false}}
-
-   :logseq.property.table/ordered-columns {:title "View ordered columns"
-                                           :schema
-                                           {:type :coll
-                                            :hide? true
-                                            :public? false}}
-
-   :logseq.property.table/sized-columns {:title "View columns settings"
-                                         :schema
-                                         {:type :map
-                                          :hide? true
-                                          :public? false}}
-
-   :logseq.property/view-for {:title "This view belongs to"
+     :logseq.property/icon {:title "Icon"
+                            :schema {:type :map}}
+     :logseq.property/public {:title "Publishing Public?"
                               :schema
-                              {:type :node
+                              {:type :checkbox
                                :hide? true
-                               :public? false}}
-   :logseq.property.asset/type {:title "File Type"
-                                :schema {:type :string
-                                         :hide? true
-                                         :public? false}
-                                :queryable? true}
-   :logseq.property.asset/size {:title "File Size"
-                                :schema {:type :raw-number
-                                         :hide? true
-                                         :public? false}
-                                :queryable? true}
-   :logseq.property.asset/checksum {:title "File checksum"
-                                    :schema {:type :string
+                               :view-context :page
+                               :public? true}}
+     :logseq.property/exclude-from-graph-view {:title "Excluded from Graph view?"
+                                               :schema
+                                               {:type :checkbox
+                                                :hide? true
+                                                :view-context :page
+                                                :public? true}}
+     :logseq.property/description {:title "Description"
+                                   :schema
+                                   {:type :default
+                                    :public? true}}
+
+     :logseq.property.view/type
+     {:title "View Type"
+      :schema
+      {:type :default
+       :public? false
+       :hide? true}
+      :closed-values
+      (mapv (fn [[db-ident value]]
+              {:db-ident db-ident
+               :value value
+               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
+            [[:logseq.property.view/type.table "Table View"]
+             [:logseq.property.view/type.list "List View"]
+             [:logseq.property.view/type.gallery "Gallery View"]])
+      :properties {:logseq.property/default-value :logseq.property.view/type.table}
+      :queryable? true
+      :rtc {:rtc/ignore-attr-when-init-upload true
+            :rtc/ignore-attr-when-init-download true
+            :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property.table/sorting {:title "View sorting"
+                                     :schema
+                                     {:type :coll
+                                      :hide? true
+                                      :public? false}
+                                     :rtc {:rtc/ignore-attr-when-init-upload true
+                                           :rtc/ignore-attr-when-init-download true
+                                           :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property.table/filters {:title "View filters"
+                                     :schema
+                                     {:type :coll
+                                      :hide? true
+                                      :public? false}
+                                     :rtc {:rtc/ignore-attr-when-init-upload true
+                                           :rtc/ignore-attr-when-init-download true
+                                           :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property.table/hidden-columns {:title "View hidden columns"
+                                            :schema
+                                            {:type :keyword
+                                             :cardinality :many
                                              :hide? true
-                                             :public? false}}
-   :logseq.property.asset/last-visit-page {:title "Last visit page"
-                                           :schema {:type :raw-number
-                                                    :hide? true
-                                                    :public? false}}
-   :logseq.property.asset/remote-metadata {:title "File remote metadata"
+                                             :public? false}
+                                            :rtc {:rtc/ignore-attr-when-init-upload true
+                                                  :rtc/ignore-attr-when-init-download true
+                                                  :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property.table/ordered-columns {:title "View ordered columns"
+                                             :schema
+                                             {:type :coll
+                                              :hide? true
+                                              :public? false}
+                                             :rtc {:rtc/ignore-attr-when-init-upload true
+                                                   :rtc/ignore-attr-when-init-download true
+                                                   :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property.table/sized-columns {:title "View columns settings"
                                            :schema
                                            {:type :map
                                             :hide? true
-                                            :public? false}}
-   :logseq.property.asset/resize-metadata {:title "Asset resize metadata"
-                                           :schema {:type :map
-                                                    :hide? true
-                                                    :public? false}}
-   :logseq.property.fsrs/due {:title "Due"
-                              :schema
-                              {:type :datetime
-                               :hide? false
-                               :public? false}}
-   :logseq.property.fsrs/state {:title "State"
+                                            :public? false}
+                                           :rtc {:rtc/ignore-attr-when-init-upload true
+                                                 :rtc/ignore-attr-when-init-download true
+                                                 :rtc/ignore-attr-when-syncing true}}
+
+     :logseq.property/view-for {:title "This view belongs to"
                                 :schema
-                                {:type :map
-                                 :hide? false ; TODO: show for debug now, hide it later
+                                {:type :node
+                                 :hide? true
                                  :public? false}}
-   :logseq.property.user/name {:title "User Name"
-                               :schema
-                               {:type :string
-                                :hide? false
-                                :public? true}}
-   :logseq.property.user/email {:title "User Email"
+     :logseq.property.asset/type {:title "File Type"
+                                  :schema {:type :string
+                                           :hide? true
+                                           :public? false}
+                                  :queryable? true}
+     :logseq.property.asset/size {:title "File Size"
+                                  :schema {:type :raw-number
+                                           :hide? true
+                                           :public? false}
+                                  :queryable? true}
+     :logseq.property.asset/checksum {:title "File checksum"
+                                      :schema {:type :string
+                                               :hide? true
+                                               :public? false}}
+     :logseq.property.asset/last-visit-page {:title "Last visit page"
+                                             :schema {:type :raw-number
+                                                      :hide? true
+                                                      :public? false}
+                                             :rtc {:rtc/ignore-attr-when-init-upload true
+                                                   :rtc/ignore-attr-when-init-download true
+                                                   :rtc/ignore-attr-when-syncing true}}
+     :logseq.property.asset/remote-metadata {:title "File remote metadata"
+                                             :schema
+                                             {:type :map
+                                              :hide? true
+                                              :public? false}
+                                             :rtc {:rtc/ignore-attr-when-init-upload true
+                                                   :rtc/ignore-attr-when-init-download true
+                                                   :rtc/ignore-attr-when-syncing true}}
+     :logseq.property.asset/resize-metadata {:title "Asset resize metadata"
+                                             :schema {:type :map
+                                                      :hide? true
+                                                      :public? false}}
+     :logseq.property.fsrs/due {:title "Due"
                                 :schema
-                                {:type :string
+                                {:type :datetime
                                  :hide? false
-                                 :public? true}}
-   :logseq.property.user/avatar {:title "User Avatar"
+                                 :public? false}}
+     :logseq.property.fsrs/state {:title "State"
+                                  :schema
+                                  {:type :map
+                                   :hide? false ; TODO: show for debug now, hide it later
+                                   :public? false}}
+     :logseq.property.user/name {:title "User Name"
                                  :schema
                                  {:type :string
                                   :hide? false
                                   :public? true}}
-   :logseq.property/enable-history? {:title "Enable property history"
-                                     :schema {:type :checkbox
-                                              :public? true
-                                              :view-context :property}}
-   :logseq.property.history/block {:title "History block"
-                                   :schema {:type :entity
-                                            :hide? true}}
-   :logseq.property.history/property {:title "History property"
-                                      :schema {:type :property
-                                               :hide? true}}
-   :logseq.property.history/ref-value {:title "History value"
-                                       :schema {:type :entity
-                                                :hide? true}}
-   :logseq.property.history/scalar-value {:title "History scalar value"
-                                          :schema {:type :any
-                                                   :hide? true}}))
+     :logseq.property.user/email {:title "User Email"
+                                  :schema
+                                  {:type :string
+                                   :hide? false
+                                   :public? true}}
+     :logseq.property.user/avatar {:title "User Avatar"
+                                   :schema
+                                   {:type :string
+                                    :hide? false
+                                    :public? true}}
+     :logseq.property/enable-history? {:title "Enable property history"
+                                       :schema {:type :checkbox
+                                                :public? true
+                                                :view-context :property}}
+     :logseq.property.history/block {:title "History block"
+                                     :schema {:type :entity
+                                              :hide? true}}
+     :logseq.property.history/property {:title "History property"
+                                        :schema {:type :property
+                                                 :hide? true}}
+     :logseq.property.history/ref-value {:title "History value"
+                                         :schema {:type :entity
+                                                  :hide? true}}
+     :logseq.property.history/scalar-value {:title "History scalar value"
+                                            :schema {:type :any
+                                                     :hide? true}}
+     :logseq.property/created-by {:title "Node created by"
+                                  :schema {;; user-uuid, why not ref?
+                                           ;; - avoid losing this attr when the user-block is deleted
+                                           ;; - related user-block maybe not exists yet in graph
+                                           :type :string
+                                           :hide? true}})))
 
 (def built-in-properties
   (->> built-in-properties*

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

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

+ 6 - 7
deps/db/src/logseq/db/frontend/validate.cljs

@@ -48,7 +48,7 @@
                                                             [(:db/ident p) v])
                                                           properties)))
                     data {:entity-map m'
-                          :errors (me/humanize (explainer [m]))}]
+                          :errors (me/humanize (explainer [(dissoc m :db/id)]))}]
                 (try
                   (pprint/pprint data)
                   (catch :default _e
@@ -63,11 +63,8 @@
   (->> errors
        (group-by #(-> % :in first))
        (map (fn [[idx errors']]
-              {:entity (let [ent (get ent-maps idx)
-                             db-id (:db/id (meta ent))]
+              {:entity (let [ent (get ent-maps idx)]
                          (cond-> ent
-                           db-id
-                           (assoc :db/id db-id)
                            ;; Provide additional page info for debugging
                            (:block/page ent)
                            (update :block/page
@@ -102,7 +99,9 @@
                   #(dissoc % :block.temp/fully-loaded?)
                   (db-malli-schema/update-properties-in-ents db ent-maps*))
         errors (binding [db-malli-schema/*db-for-validate-fns* db]
-                 (-> ent-maps closed-db-schema-explainer :errors))]
+                 (-> (map (fn [e]
+                            (dissoc e :db/id))
+                          ent-maps) closed-db-schema-explainer :errors))]
     (cond-> {:datom-count (count datoms)
              :entities ent-maps*}
       (some? errors)
@@ -123,4 +122,4 @@
      :properties properties-count
      ;; Objects that aren't classes or properties
      :objects (- (count (d/datoms db :avet :block/tags)) classes-count properties-count)
-     :property-pairs (count (mapcat #(-> % db-property/properties (dissoc :block/tags)) entities))}))
+     :property-pairs (count (mapcat #(-> % db-property/properties (dissoc :block/tags)) entities))}))

+ 1 - 1
deps/db/src/logseq/db/sqlite/common_db.cljs

@@ -29,7 +29,7 @@
   (->> (d/datoms db :avet :block/title page-name)
        (filter (fn [d]
                  (let [e (d/entity db (:e d))]
-                   (or (entity-util/page? e) (:block/tags e)))))
+                   (entity-util/page? e))))
        (map :e)
        sort
        first))

+ 7 - 7
deps/db/test/logseq/db/sqlite/create_graph_test.cljs

@@ -1,16 +1,16 @@
 (ns logseq.db.sqlite.create-graph-test
   (:require [cljs.test :refer [deftest is testing]]
-            [clojure.string :as string]
             [clojure.set :as set]
+            [clojure.string :as string]
             [datascript.core :as d]
+            [logseq.db :as ldb]
+            [logseq.db.frontend.class :as db-class]
+            [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.schema :as db-schema]
-            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [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.test.helper :as db-test]
-            [logseq.db.frontend.class :as db-class]))
+            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
+            [logseq.db.test.helper :as db-test]))
 
 (deftest new-graph-db-idents
   (testing "a new graph follows :db/ident conventions for"
@@ -69,7 +69,7 @@
         task (d/entity @conn :logseq.class/Task)]
     (is (ldb/class? task)
         "Task class has correct type")
-    (is (= 3 (count (:logseq.property.class/properties task)))
+    (is (= 4 (count (:logseq.property.class/properties task)))
         "Has correct number of task properties")
     (is (every? ldb/property? (:logseq.property.class/properties task))
         "Each task property has correct type")))

+ 31 - 17
deps/graph-parser/script/db_import.cljs

@@ -2,21 +2,28 @@
   "Imports given file(s) to a db graph. This script is primarily for
    developing the import feature and for engineers who want to customize
    the import process"
-  (:require [clojure.string :as string]
-            [datascript.core :as d]
-            ["path" :as node-path]
-            ["os" :as os]
-            ["fs" :as fs]
+  (:require ["fs" :as fs]
             ["fs/promises" :as fsp]
-            [nbb.core :as nbb]
-            [nbb.classpath :as cp]
+            ["os" :as os]
+            ["path" :as node-path]
+            #_:clj-kondo/ignore
             [babashka.cli :as cli]
-            [logseq.graph-parser.exporter :as gp-exporter]
+            [cljs.pprint :as pprint]
+            [clojure.set :as set]
+            [clojure.string :as string]
+            [datascript.core :as d]
             [logseq.common.graph :as common-graph]
-            #_:clj-kondo/ignore
+            [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.outliner.cli :as outliner-cli]
-            [promesa.core :as p]
-            [clojure.set :as set]))
+            [nbb.classpath :as cp]
+            [nbb.core :as nbb]
+            [promesa.core :as p]))
+
+(def last-tx-data (atom nil))
+(def original-transact! d/transact!)
+(defn dev-transact! [conn tx-data tx-meta]
+  (reset! last-tx-data tx-data)
+  (original-transact! conn tx-data tx-meta))
 
 (defn- build-graph-files
   "Given a file graph directory, return all files including assets and adds relative paths
@@ -41,7 +48,7 @@
           _ (fsp/mkdir parent-dir #js {:recursive true})]
     (fsp/copyFile (:path file) (node-path/join parent-dir (node-path/basename (:path file))))))
 
-(defn- notify-user [{:keys [continue]} m]
+(defn- notify-user [{:keys [continue debug]} m]
   (println (:msg m))
   (when (:ex-data m)
     (println "Ex-data:" (pr-str (dissoc (:ex-data m) :error)))
@@ -55,7 +62,10 @@
                        (when (:sci.impl/f-meta %)
                          (str " calls #'" (get-in % [:sci.impl/f-meta :ns]) "/" (get-in % [:sci.impl/f-meta :name]))))
                  (reverse stack))))
-      (println (some-> (get-in m [:ex-data :error]) .-stack))))
+      (println (some-> (get-in m [:ex-data :error]) .-stack)))
+    (when debug
+      (println "Last Tx Data:")
+      (pprint/pprint @last-tx-data)))
   (when (and (= :error (:level m)) (not continue))
     (js/process.exit 1)))
 
@@ -83,7 +93,8 @@
                         ;; asset file options
                        {:<copy-asset (fn copy-asset [file]
                                        (<copy-asset-file file db-graph-dir file-graph-dir))})]
-    (gp-exporter/export-file-graph conn conn config-file *files options)))
+    (p/with-redefs [d/transact! dev-transact!]
+      (gp-exporter/export-file-graph conn conn config-file *files options))))
 
 (defn- resolve-path
   "If relative path, resolve with $ORIGINAL_PWD"
@@ -98,8 +109,9 @@
   (let [doc-options (gp-exporter/build-doc-options {:macros {}} (merge options (default-export-options options)))
         files' (mapv #(hash-map :path %)
                      (into [file] (map resolve-path files)))]
-    (p/let [_ (gp-exporter/export-doc-files conn files' <read-file doc-options)]
-      {:import-state (:import-state doc-options)})))
+    (p/with-redefs [d/transact! dev-transact!]
+      (p/let [_ (gp-exporter/export-doc-files conn files' <read-file doc-options)]
+        {:import-state (:import-state doc-options)}))))
 
 (def spec
   "Options spec"
@@ -107,6 +119,8 @@
           :desc "Print help"}
    :verbose {:alias :v
              :desc "Verbose mode"}
+   :debug {:alias :d
+           :desc "Debug mode"}
    :continue {:alias :c
               :desc "Continue past import failures"}
    :all-tags {:alias :a
@@ -150,7 +164,7 @@
         _ (when (:verbose options) (prn :options user-options))
         options' (merge {:user-options user-options
                          :graph-name db-name}
-                        (select-keys options [:files :verbose :continue]))]
+                        (select-keys options [:files :verbose :continue :debug]))]
     (p/let [{:keys [import-state]}
             (if directory?
               (import-file-graph-to-db file-graph' (node-path/join dir db-name) conn options')

+ 40 - 28
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -731,28 +731,29 @@
 
 (defn- handle-page-properties
   "Adds page properties including special handling for :logseq.property/parent"
-  [{:block/keys [properties] :as block*} db {:keys [page-names-to-uuids]} refs
+  [{:block/keys [properties] :as block*} db {:keys [page-names-to-uuids classes-tx]} refs
    {:keys [user-options log-fn import-state] :as options}]
   (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
         block'
-        (if (seq properties)
-          (let [parent-classes-from-properties (->> (select-keys properties (:property-parent-classes user-options))
-                                                    (mapcat (fn [[_k v]] (if (coll? v) v [v])))
-                                                    distinct)]
-            ;; TODO: Mv new classes from these find-or-create-class to :classes-tx as they are the only ones
-            ;; that aren't conrolled by :classes-tx
-            (cond-> block
-              (seq parent-classes-from-properties)
-              (merge (find-or-create-class db ((some-fn ::original-title :block/title) block) (:all-idents import-state) block))
-              (seq parent-classes-from-properties)
-              (assoc :logseq.property/parent
-                     (let [new-class (first parent-classes-from-properties)
-                           class-m (find-or-create-class db new-class (:all-idents import-state))]
-                       (when (> (count parent-classes-from-properties) 1)
-                         (log-fn :skipped-parent-classes "Only one parent class is allowed so skipped ones after the first one" :classes parent-classes-from-properties))
-                       (merge class-m
-                              {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m))})))))
-          (dissoc block* :block/properties))
+        (if-let [parent-classes-from-properties (->> (select-keys properties (:property-parent-classes user-options))
+                                                     (mapcat (fn [[_k v]] (if (coll? v) v [v])))
+                                                     distinct
+                                                     seq)]
+          (let [_ (swap! (:classes-from-property-parents import-state) conj (:block/title block*))
+                class-m (find-or-create-class db ((some-fn ::original-title :block/title) block) (:all-idents import-state) block)
+                class-m' (-> block
+                             (merge class-m)
+                             (assoc :logseq.property/parent
+                                    (let [new-class (first parent-classes-from-properties)
+                                          class-m (find-or-create-class db new-class (:all-idents import-state))
+                                          class-m' (merge class-m
+                                                          {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (common-util/page-name-sanity-lc new-class) (:db/ident class-m))})]
+                                      (when (> (count parent-classes-from-properties) 1)
+                                        (log-fn :skipped-parent-classes "Only one parent class is allowed so skipped ones after the first one" :classes parent-classes-from-properties))
+                                      (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
+                                      [:block/uuid (:block/uuid class-m')])))]
+            class-m')
+          block)
         block'' (replace-namespace-with-parent block' page-names-to-uuids)]
     {:block block'' :properties-tx properties-tx}))
 
@@ -915,7 +916,7 @@
   "Returns a map of unique page names mapped to their uuids. The page names
    are in a format that is compatible with extract/extract e.g. namespace pages have
    their full hierarchy in the name"
-  [db]
+  [db classes-from-property-parents]
   (->> db
        ;; don't fetch built-in as that would give the wrong entity if a user used
        ;; a db-only built-in property name e.g. description
@@ -924,6 +925,8 @@
        (map #(d/entity db %))
        (map #(vector
               (if-let [parents (and (or (ldb/internal-page? %) (ldb/class? %))
+                                    ;; These classes have parents now but don't in file graphs (and in extract)
+                                    (not (contains? classes-from-property-parents (:block/title %)))
                                     (->> (ldb/get-page-parents %)
                                          (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
                                          seq))]
@@ -1000,7 +1003,7 @@
                         ;; remove file path relative
                         (map #(dissoc % :block/file)))
         ;; Fetch all named ents once per import file to speed up named lookups
-        all-existing-page-uuids (get-all-existing-page-uuids @conn)
+        all-existing-page-uuids (get-all-existing-page-uuids @conn @(:classes-from-property-parents import-state))
         all-pages (map #(modify-page-tx % all-existing-page-uuids) all-pages*)
         all-new-page-uuids (->> all-pages
                                 (remove #(all-existing-page-uuids (or (::original-name %) (:block/name %))))
@@ -1110,6 +1113,8 @@
    :property-schemas (atom {})
    ;; Map of property or class names (keyword) to db-ident keywords
    :all-idents (atom {})
+   ;; Set of children pages turned into classes by :property-parent-classes option
+   :classes-from-property-parents (atom #{})
    ;; Map of block uuids to their :block/properties-text-values value.
    ;; Used if a property value changes to :default
    :block-properties-text-values (atom {})})
@@ -1262,7 +1267,7 @@
 (defn- clean-extra-invalid-tags
   "If a page/class tx is an existing property or a new or existing class, ensure that
   it only has one tag by removing :logseq.class/Page from its tx"
-  [db pages-tx' classes-tx]
+  [db pages-tx' classes-tx existing-pages]
   ;; TODO: Improve perf if we tracked all created classes in atom
   (let [existing-classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
                               (map #(d/entity db (:e %)))
@@ -1273,7 +1278,13 @@
         existing-properties (->> (d/datoms db :avet :block/tags :logseq.class/Property)
                                  (map #(d/entity db (:e %)))
                                  (map :block/uuid)
-                                 set)]
+                                 set)
+        existing-pages' (set/map-invert existing-pages)
+        retract-page-tag-from-existing-pages
+        (->> pages-tx'
+             ;; Existing pages that have converted to property or class
+             (filter #(and (:db/ident %) (get existing-pages' (:block/uuid %))))
+             (mapv #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page)))]
     {:pages-tx
      (mapv (fn [page]
              (if (or (contains? classes (:block/uuid page))
@@ -1281,9 +1292,10 @@
                (update page :block/tags (fn [tags] (vec (remove #(= % :logseq.class/Page) tags))))
                page))
            pages-tx')
-     :retract-page-tag-from-classes-tx
-     (mapv #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page)
-           classes-tx)}))
+     :retract-page-tags-tx
+     (into (mapv #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page)
+                 classes-tx)
+           retract-page-tag-from-existing-pages)}))
 
 (defn add-file-to-db-graph
   "Parse file and save parsed data to the given db graph. Options available:
@@ -1326,8 +1338,8 @@
         main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true})
 
         classes-tx @(:classes-tx tx-options)
-        {:keys [retract-page-tag-from-classes-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx)
-        classes-tx' (concat classes-tx retract-page-tag-from-classes-tx)
+        {:keys [retract-page-tags-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx existing-pages)
+        classes-tx' (concat classes-tx retract-page-tags-tx)
         ;; Build indices
         pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx'')
                          (concat (map #(select-keys % [:block/uuid]) classes-tx))

+ 16 - 10
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -177,7 +177,7 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.task/deadline
-      (is (= 24 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+      (is (= 25 (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 (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
@@ -195,7 +195,7 @@
                   #_(map #(select-keys % [:block/title :block/tags]))
                   count))
           "Correct number of pages with block content")
-      (is (= 11 (->> @conn
+      (is (= 12 (->> @conn
                      (d/q '[:find [?ident ...]
                             :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
                      count))
@@ -293,12 +293,16 @@
                (and b (readable-properties @conn b)))
             ":template properties are ignored to not invalidate its property types"))
 
-      (is (= {:logseq.task/deadline (date-time-util/journal-day->ms 20221126)}
-             (readable-properties @conn (db-test/find-block-by-content @conn "only deadline")))
+      (is (= 20221126
+             (-> (readable-properties @conn (db-test/find-block-by-content @conn "only deadline"))
+                 :logseq.task/deadline
+                 date-time-util/ms->journal-day))
           "deadline block has correct journal as property value")
 
-      (is (= {:logseq.task/deadline (date-time-util/journal-day->ms 20221125)}
-             (readable-properties @conn (db-test/find-block-by-content @conn "only scheduled")))
+      (is (= 20221125
+             (-> (readable-properties @conn (db-test/find-block-by-content @conn "only scheduled"))
+                 :logseq.task/deadline
+                 date-time-util/ms->journal-day))
           "scheduled block converted to correct deadline")
 
       (is (= 1 (count (d/q '[:find [(pull ?b [*]) ...]
@@ -655,8 +659,10 @@
 
 (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"
-                                                          "pages/Whiteboard___Tool.md" "pages/Whiteboard___Arrow_head_toggle.md"])
+          files (mapv #(node-path/join file-graph-dir %) ["journals/2024_11_26.md"
+                                                          "pages/CreativeWork.md" "pages/Movie.md" "pages/type.md"
+                                                          "pages/Whiteboard___Tool.md" "pages/Whiteboard___Arrow_head_toggle.md"
+                                                          "pages/Property.md" "pages/url.md"])
           conn (db-test/create-conn)
           _ (import-files-to-db files conn {:property-parent-classes ["parent"]
                                             ;; Also add this option to trigger some edge cases with namespace pages
@@ -665,8 +671,8 @@
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
 
-    (is (= #{:user.class/Movie :user.class/CreativeWork :user.class/Thing
-             :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool}
+    (is (= #{:user.class/Movie :user.class/CreativeWork :user.class/Thing :user.class/Feature
+             :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool :user.class/Property}
            (->> @conn
                 (d/q '[:find [?ident ...]
                        :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])

+ 2 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_11_26.md

@@ -6,4 +6,5 @@
   card-ease-factor:: 2.36
   card-last-reviewed:: 2024-11-26T20:27:33.373Z
 - card 2 with cloze {{cloze surprise!}} #card
-  card-next-schedule:: 2024-11-27T05:00:00.000Z
+  card-next-schedule:: 2024-11-27T05:00:00.000Z
+- This block references a page with a future parent class #Property

+ 1 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2025_01_10.md

@@ -0,0 +1 @@
+- A page ref with same as a block (task in this case) shouldn't fail [[do X]]

+ 1 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/Property.md

@@ -0,0 +1 @@
+parent:: [[Thing]]

+ 1 - 1
deps/graph-parser/test/resources/exporter-test-graph/pages/url.md

@@ -1,4 +1,4 @@
 type:: [[Property]]
 url:: {{docs-base-url url}}
 sameAs:: https://schema.org/url
-rangeIncludes:: [[Uri]]
+rangeIncludes:: [[Property]]

+ 4 - 6
deps/outliner/src/logseq/outliner/pipeline.cljs

@@ -1,11 +1,9 @@
 (ns logseq.outliner.pipeline
   "Core fns for use with frontend worker and node"
-  (:require [cljs-time.coerce :as tc]
-            [cljs-time.core :as t]
-            [cljs-time.format :as tf]
-            [clojure.set :as set]
+  (:require [clojure.set :as set]
             [datascript.core :as d]
             [datascript.impl.entity :as de]
+            [logseq.common.util.date-time :as date-time-util]
             [logseq.db :as ldb]
             [logseq.db.frontend.content :as db-content]
             [logseq.db.frontend.entity-plus :as entity-plus]
@@ -147,8 +145,8 @@
 
 (defn ^:api get-journal-day-from-long
   [db v]
-  (when-let [date (t/to-default-time-zone (tc/from-long v))]
-    (let [day (js/parseInt (tf/unparse (tf/formatter "yyyyMMdd") date))]
+  (when v
+    (let [day (date-time-util/ms->journal-day v)]
       (:e (first (d/datoms db :avet :block/journal-day day))))))
 
 (defn db-rebuild-block-refs

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.2.0",
+  "version": "0.2.1",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

+ 10 - 1
libs/src/LSPlugin.core.ts

@@ -512,6 +512,10 @@ class PluginLocal extends EventEmitter<
 
     if (this.isWebPlugin) {
       // TODO: strategy for Logseq plugins center
+      if (this.installedFromUserWebUrl) {
+        return `${this.installedFromUserWebUrl}/${filePath}`
+      }
+
       return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(localRoot, filePath)}`
     }
 
@@ -565,7 +569,7 @@ class PluginLocal extends EventEmitter<
       })
 
     const { repo, version } = this._options
-    const localRoot = (this._localRoot = this.isWebPlugin ? `${repo}/${version}` : safetyPathNormalize(url))
+    const localRoot = (this._localRoot = this.isWebPlugin ? `${repo || url}/${version}` : safetyPathNormalize(url))
     const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
     const validateEntry = (main) => main && /\.(js|html)$/.test(main)
 
@@ -994,6 +998,10 @@ class PluginLocal extends EventEmitter<
     return this._ctx.isWebPlatform || !!this.options.webPkg
   }
 
+  get installedFromUserWebUrl() {
+    return this.isWebPlugin && this.options.webPkg?.installedFromUserWebUrl
+  }
+
   get layoutCore(): any {
     // @ts-expect-error
     return window.frontend.modules.layout.core
@@ -1104,6 +1112,7 @@ class PluginLocal extends EventEmitter<
     json.err = this.loadErr
     json.usf = this.dotSettingsFile
     json.iir = this.isInstalledInLocalDotRoot
+    json.webMode = this.isWebPlugin ? (this.installedFromUserWebUrl ? 'user' : 'github') : false
     json.lsr = this._resolveResourceFullUrl('/')
 
     if (settings === false) {

+ 12 - 1
libs/src/LSPlugin.ts

@@ -145,6 +145,7 @@ export interface AppUserInfo {
 
 export interface AppInfo {
   version: string
+  supportDb: boolean
 
   [key: string]: unknown
 }
@@ -314,7 +315,7 @@ export type ExternalCommandType =
   | 'logseq.ui/toggle-theme'
   | 'logseq.ui/toggle-wide-mode'
 
-export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
+export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets' | 'utils'
 
 export type SearchIndiceInitStatus = boolean
 export type SearchBlockItem = {
@@ -686,6 +687,8 @@ export interface IEditorProxy extends Record<string, any> {
    */
   newBlockUUID: () => Promise<string>
 
+  isPageBlock: (block: BlockEntity | PageEntity) => Boolean
+
   /**
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
    *
@@ -761,6 +764,10 @@ export interface IEditorProxy extends Record<string, any> {
     }>
   ) => Promise<PageEntity | null>
 
+  createJournalPage: (
+    date: string | Date
+  ) => Promise<PageEntity | null>
+
   deletePage: (pageName: BlockPageName) => Promise<void>
 
   renamePage: (oldName: string, newName: string) => Promise<void>
@@ -930,6 +937,10 @@ export interface IUIProxy {
   resolveThemeCssPropsVals: (props: string | Array<string>) => Promise<Record<string, string | undefined> | null>
 }
 
+export interface IUtilsProxy {
+  toJs: <R = unknown>(obj: {}) => Promise<R>
+}
+
 /**
  * Assets related APIs
  */

+ 97 - 72
libs/src/LSPlugin.user.ts

@@ -37,6 +37,7 @@ import {
   IAssetsProxy,
   AppInfo,
   IPluginSearchServiceHooks,
+  PageEntity, IUtilsProxy,
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -64,7 +65,7 @@ const logger = new PluginLogger('', { console: true })
  * @param opts
  * @param action
  */
-function registerSimpleCommand (
+function registerSimpleCommand(
   this: LSPluginUser,
   type: string,
   opts: {
@@ -109,7 +110,7 @@ function registerSimpleCommand (
   })
 }
 
-function shouldValidUUID (uuid: string) {
+function shouldValidUUID(uuid: string) {
   if (!isValidUUID(uuid)) {
     logger.error(`#${uuid} is not a valid UUID string.`)
     return false
@@ -118,7 +119,7 @@ function shouldValidUUID (uuid: string) {
   return true
 }
 
-function checkEffect (p: LSPluginUser) {
+function checkEffect(p: LSPluginUser) {
   return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
 }
 
@@ -126,7 +127,7 @@ let _appBaseInfo: AppInfo = null
 let _searchServices: Map<string, LSPluginSearchService> = new Map()
 
 const app: Partial<IAppProxy> = {
-  async getInfo (this: LSPluginUser, key) {
+  async getInfo(this: LSPluginUser, key) {
     if (!_appBaseInfo) {
       _appBaseInfo = await this._execCallableAPIAsync('get-app-info')
     }
@@ -135,7 +136,7 @@ const app: Partial<IAppProxy> = {
 
   registerCommand: registerSimpleCommand,
 
-  registerSearchService<T extends IPluginSearchServiceHooks> (
+  registerSearchService<T extends IPluginSearchServiceHooks>(
     this: LSPluginUser,
     s: T
   ) {
@@ -146,7 +147,7 @@ const app: Partial<IAppProxy> = {
     _searchServices.set(s.name, new LSPluginSearchService(this, s))
   },
 
-  registerCommandPalette (
+  registerCommandPalette(
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback
   ) {
@@ -161,7 +162,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  registerCommandShortcut (
+  registerCommandShortcut(
     keybinding: SimpleCommandKeybinding | string,
     action: SimpleCommandCallback,
     opts: Partial<{
@@ -190,7 +191,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  registerUIItem (
+  registerUIItem(
     type: 'toolbar' | 'pagebar',
     opts: { key: string; template: string }
   ) {
@@ -203,7 +204,7 @@ const app: Partial<IAppProxy> = {
     })
   },
 
-  registerPageMenuItem (
+  registerPageMenuItem(
     this: LSPluginUser,
     tag: string,
     action: (e: IHookEvent & { page: string }) => void
@@ -227,7 +228,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  onBlockRendererSlotted (uuid, callback: (payload: any) => void) {
+  onBlockRendererSlotted(uuid, callback: (payload: any) => void) {
     if (!shouldValidUUID(uuid)) return
 
     const pid = this.baseInfo.id
@@ -242,7 +243,7 @@ const app: Partial<IAppProxy> = {
     }
   },
 
-  invokeExternalPlugin (this: LSPluginUser, type: string, ...args: Array<any>) {
+  invokeExternalPlugin(this: LSPluginUser, type: string, ...args: Array<any>) {
     type = type?.trim()
     if (!type) return
     let [pid, group] = type.split('.')
@@ -263,7 +264,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  setFullScreen (flag) {
+  setFullScreen(flag) {
     const sf = (...args) => this._callWin('setFullScreen', ...args)
 
     if (flag === 'toggle') {
@@ -279,11 +280,18 @@ const app: Partial<IAppProxy> = {
 let registeredCmdUid = 0
 
 const editor: Partial<IEditorProxy> = {
-  newBlockUUID (this: LSPluginUser): Promise<string> {
+  newBlockUUID(this: LSPluginUser): Promise<string> {
     return this._execCallableAPIAsync('new_block_uuid')
   },
 
-  registerSlashCommand (
+  isPageBlock(
+    this: LSPluginUser,
+    block: BlockEntity | PageEntity
+  ): Boolean {
+    return block.uuid && block.hasOwnProperty('name')
+  },
+
+  registerSlashCommand(
     this: LSPluginUser,
     tag: string,
     actions: BlockCommandCallback | Array<SlashCommandAction>
@@ -331,7 +339,7 @@ const editor: Partial<IEditorProxy> = {
     })
   },
 
-  registerBlockContextMenuItem (
+  registerBlockContextMenuItem(
     this: LSPluginUser,
     label: string,
     action: BlockCommandCallback
@@ -354,7 +362,7 @@ const editor: Partial<IEditorProxy> = {
     )
   },
 
-  registerHighlightContextMenuItem (
+  registerHighlightContextMenuItem(
     this: LSPluginUser,
     label: string,
     action: SimpleCommandCallback,
@@ -379,7 +387,7 @@ const editor: Partial<IEditorProxy> = {
     )
   },
 
-  scrollToBlockInPage (
+  scrollToBlockInPage(
     this: LSPluginUser,
     pageName: BlockPageName,
     blockId: BlockIdentity,
@@ -395,7 +403,7 @@ const editor: Partial<IEditorProxy> = {
 }
 
 const db: Partial<IDBProxy> = {
-  onBlockChanged (
+  onBlockChanged(
     this: LSPluginUser,
     uuid: BlockUUID,
     callback: (
@@ -425,7 +433,7 @@ const db: Partial<IDBProxy> = {
     }
   },
 
-  datascriptQuery<T = any> (
+  datascriptQuery<T = any>(
     this: LSPluginUser,
     query: string,
     ...inputs: Array<any>
@@ -446,8 +454,10 @@ const git: Partial<IGitProxy> = {}
 
 const ui: Partial<IUIProxy> = {}
 
+const utils: Partial<IUtilsProxy> = {}
+
 const assets: Partial<IAssetsProxy> = {
-  makeSandboxStorage (this: LSPluginUser): IAsyncStorage {
+  makeSandboxStorage(this: LSPluginUser): IAsyncStorage {
     return new LSPluginFileStorage(this, { assets: true })
   },
 }
@@ -496,7 +506,7 @@ export class LSPluginUser
    * @param _baseInfo
    * @param _caller
    */
-  constructor (
+  constructor(
     private _baseInfo: LSPluginBaseInfo,
     private _caller: LSPluginCaller
   ) {
@@ -529,7 +539,7 @@ export class LSPluginUser
   }
 
   // Life related
-  async ready (model?: any, callback?: any) {
+  async ready(model?: any, callback?: any) {
     if (this._connected) return
 
     try {
@@ -576,39 +586,39 @@ export class LSPluginUser
     }
   }
 
-  ensureConnected () {
+  ensureConnected() {
     if (!this._connected) {
       throw new Error('not connected')
     }
   }
 
-  beforeunload (callback: (e: any) => Promise<void>): void {
+  beforeunload(callback: (e: any) => Promise<void>): void {
     if (typeof callback !== 'function') return
     this._beforeunloadCallback = callback
   }
 
-  provideModel (model: Record<string, any>) {
+  provideModel(model: Record<string, any>) {
     this.caller._extendUserModel(model)
     return this
   }
 
-  provideTheme (theme: Theme) {
+  provideTheme(theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }
 
-  provideStyle (style: StyleString) {
+  provideStyle(style: StyleString) {
     this.caller.call('provider:style', style)
     return this
   }
 
-  provideUI (ui: UIOptions) {
+  provideUI(ui: UIOptions) {
     this.caller.call('provider:ui', ui)
     return this
   }
 
   // Settings related
-  useSettingsSchema (schema: Array<SettingSchemaDesc>) {
+  useSettingsSchema(schema: Array<SettingSchemaDesc>) {
     if (this.connected) {
       this.caller.call('settings:schema', {
         schema,
@@ -620,35 +630,35 @@ export class LSPluginUser
     return this
   }
 
-  updateSettings (attrs: Record<string, any>) {
+  updateSettings(attrs: Record<string, any>) {
     this.caller.call('settings:update', attrs)
     // TODO: update associated baseInfo settings
   }
 
-  onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook {
+  onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
     const type = 'settings:changed'
     this.on(type, cb)
     return () => this.off(type, cb)
   }
 
-  showSettingsUI () {
+  showSettingsUI() {
     this.caller.call('settings:visible:changed', { visible: true })
   }
 
-  hideSettingsUI () {
+  hideSettingsUI() {
     this.caller.call('settings:visible:changed', { visible: false })
   }
 
   // UI related
-  setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
+  setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }
 
-  setMainUIInlineStyle (style: CSS.Properties): void {
+  setMainUIInlineStyle(style: CSS.Properties): void {
     this.caller.call('main-ui:style', style)
   }
 
-  hideMainUI (opts?: { restoreEditingCursor: boolean }): void {
+  hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: false,
@@ -659,7 +669,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  showMainUI (opts?: { autoFocus: boolean }): void {
+  showMainUI(opts?: { autoFocus: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: true,
@@ -670,7 +680,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  toggleMainUI (): void {
+  toggleMainUI(): void {
     const payload = { key: KEY_MAIN_UI, toggle: true }
     const state = this._ui.get(payload.key)
     if (state && state.visible) {
@@ -681,40 +691,40 @@ export class LSPluginUser
   }
 
   // Getters
-  get version (): string {
+  get version(): string {
     return this._version
   }
 
-  get isMainUIVisible (): boolean {
+  get isMainUIVisible(): boolean {
     const state = this._ui.get(KEY_MAIN_UI)
     return Boolean(state && state.visible)
   }
 
-  get connected (): boolean {
+  get connected(): boolean {
     return this._connected
   }
 
-  get baseInfo (): LSPluginBaseInfo {
+  get baseInfo(): LSPluginBaseInfo {
     return this._baseInfo
   }
 
-  get effect (): Boolean {
+  get effect(): Boolean {
     return checkEffect(this)
   }
 
-  get logger () {
+  get logger() {
     return logger
   }
 
-  get settings () {
+  get settings() {
     return this.baseInfo?.settings
   }
 
-  get caller (): LSPluginCaller {
+  get caller(): LSPluginCaller {
     return this._caller
   }
 
-  resolveResourceFullUrl (filePath: string) {
+  resolveResourceFullUrl(filePath: string) {
     this.ensureConnected()
     if (!filePath) return
     filePath = filePath.replace(/^[.\\/]+/, '')
@@ -724,17 +734,18 @@ export class LSPluginUser
   /**
    * @internal
    */
-  _makeUserProxy (target: any, tag?: UserProxyTags) {
+  _makeUserProxy(target: any, tag?: UserProxyTags) {
     const that = this
     const caller = this.caller
 
     return new Proxy(target, {
-      get (target: any, propKey, receiver) {
+      get(target: any, propKey, _receiver) {
         const origMethod = target[propKey]
 
         return function (this: any, ...args: any) {
           if (origMethod) {
-            const ret = origMethod.apply(that, args.concat(tag))
+            if (args?.length !== 0) args.concat(tag)
+            const ret = origMethod.apply(that, args)
             if (ret !== PROXY_CONTINUE) return ret
           }
 
@@ -784,7 +795,8 @@ export class LSPluginUser
 
           let method = propKey as string
 
-          if ((['git', 'ui', 'assets'] as UserProxyTags[]).includes(tag)) {
+          // TODO: refactor api call with the explicit tag
+          if ((['git', 'ui', 'assets', 'utils'] as UserProxyTags[]).includes(tag)) {
             method = tag + '_' + method
           }
 
@@ -799,64 +811,77 @@ export class LSPluginUser
     })
   }
 
-  _execCallableAPIAsync (method: callableMethods, ...args) {
+  _execCallableAPIAsync(method: callableMethods, ...args) {
     return this._caller.callAsync(`api:call`, {
       method,
       args,
     })
   }
 
-  _execCallableAPI (method: callableMethods, ...args) {
+  _execCallableAPI(method: callableMethods, ...args) {
     this._caller.call(`api:call`, {
       method,
       args,
     })
   }
 
-  _callWin (...args) {
+  _callWin(...args) {
     return this._execCallableAPIAsync(`_callMainWin`, ...args)
   }
 
-  /**
-   * The interface methods of {@link IAppProxy}
-   */
-  get App (): IAppProxy {
-    return this._makeUserProxy(app, 'app')
+  // User Proxies
+  #appProxy: IAppProxy
+  #editorProxy: IEditorProxy
+  #dbProxy: IDBProxy
+  #uiProxy: IUIProxy
+  #utilsProxy: IUtilsProxy
+
+  get App(): IAppProxy {
+    if (this.#appProxy) return this.#appProxy
+    return (this.#appProxy = this._makeUserProxy(app, 'app'))
   }
 
-  get Editor (): IEditorProxy {
-    return this._makeUserProxy(editor, 'editor')
+  get Editor(): IEditorProxy {
+    if (this.#editorProxy) return this.#editorProxy
+    return (this.#editorProxy = this._makeUserProxy(editor, 'editor'))
   }
 
-  get DB (): IDBProxy {
-    return this._makeUserProxy(db, 'db')
+  get DB(): IDBProxy {
+    if (this.#dbProxy) return this.#dbProxy
+    return (this.#dbProxy = this._makeUserProxy(db, 'db'))
   }
 
-  get Git (): IGitProxy {
-    return this._makeUserProxy(git, 'git')
+  get UI(): IUIProxy {
+    if (this.#uiProxy) return this.#uiProxy
+    return (this.#uiProxy = this._makeUserProxy(ui, 'ui'))
   }
 
-  get UI (): IUIProxy {
-    return this._makeUserProxy(ui, 'ui')
+  get Utils(): IUtilsProxy {
+    if (this.#utilsProxy) return this.#utilsProxy
+    return (this.#utilsProxy = this._makeUserProxy(utils, 'utils'))
+  }
+
+  get Git(): IGitProxy {
+    return this._makeUserProxy(git, 'git')
   }
 
-  get Assets (): IAssetsProxy {
+  get Assets(): IAssetsProxy {
     return this._makeUserProxy(assets, 'assets')
   }
 
-  get FileStorage (): LSPluginFileStorage {
+  get FileStorage(): LSPluginFileStorage {
     let m = this._mFileStorage
     if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
     return m
   }
 
-  get Request (): LSPluginRequest {
+  get Request(): LSPluginRequest {
     let m = this._mRequest
     if (!m) m = this._mRequest = new LSPluginRequest(this)
     return m
   }
 
-  get Experiments (): LSPluginExperiments {
+  get Experiments(): LSPluginExperiments {
     let m = this._mExperiments
     if (!m) m = this._mExperiments = new LSPluginExperiments(this)
     return m
@@ -868,7 +893,7 @@ export * from './LSPlugin'
 /**
  * @internal
  */
-export function setupPluginUserInstance (
+export function setupPluginUserInstance(
   pluginBaseInfo: LSPluginBaseInfo,
   pluginCaller: LSPluginCaller
 ) {

+ 16 - 3
libs/src/modules/LSPlugin.Experiments.ts

@@ -25,6 +25,18 @@ export class LSPluginExperiments {
     }
   }
 
+  get Utils() {
+    const utils = this.ensureHostScope().logseq.sdk.utils
+    const withCall = (name: string): (input: any) => any => utils[safeSnakeCase(name)]
+    return {
+      toClj: withCall('toClj'),
+      jsxToClj: withCall('jsxToClj'),
+      toJs: withCall('toJs'),
+      toKeyword: withCall('toKeyword'),
+      toSymbol: withCall('toSymbol')
+    }
+  }
+
   get pluginLocal(): PluginLocal {
     return this.ensureHostScope().LSPluginCore.ensurePlugin(
       this.ctx.baseInfo.id
@@ -124,11 +136,12 @@ export class LSPluginExperiments {
   }
 
   ensureHostScope(): any {
-    if (window === top) {
+    try {
+      const _ = window.top?.document
+    } catch (_e) {
       console.error('Can not access host scope!')
-      return {}
     }
 
-    return top
+    return window.top
   }
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 27
resources/js/lsplugin.core.js


+ 1 - 1
resources/js/lsplugin.core.js.LICENSE.txt

@@ -1 +1 @@
-/*! @license DOMPurify 2.3.8 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.8/LICENSE */
+/*! @license DOMPurify 2.5.4 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.5.4/LICENSE */

+ 5 - 3
scripts/src/logseq/tasks/dev/db_and_file_graphs.clj

@@ -92,7 +92,7 @@
                        ;; from logseq.db.frontend.schema
                        [:block/namespace :block/properties-text-values :block/pre-block :recent/pages :block/file :block/properties-order
                         :block/repeated :block/deadline :block/scheduled :block/priority :block/marker :block/macros
-                        :block/type]
+                        :block/type :block/format]
                        (map str)
                        (into [;; e.g. block/properties :title
                               "block/properties :"
@@ -108,7 +108,8 @@
         allowed-exceptions #{"{:block/name page-title})))"
                              "{:block/name page-title})"
                              "(when-not (db/get-page journal)"
-                             "(let [value (if datetime? (tc/to-long d) (db/get-page journal))]"}
+                             "(let [value (if datetime? (tc/to-long d) (db/get-page journal))]"
+                             "(dissoc :block/format))]"}
         res (apply shell {:out :string :continue true}
                    "git grep -E" (str "(" (string/join "|" file-concepts) ")")
                    db-graph-paths)
@@ -125,7 +126,8 @@
   []
   (let [db-concepts
         ;; from logseq.db.frontend.schema
-        ["closed-value" "class/properties" "schema.classes" "property/parent"]
+        ["closed-value" "class/properties" "schema.classes" "property/parent"
+         "logseq.property" "logseq.class"]
         res (apply shell {:out :string :continue true}
                    "git grep -E" (str "(" (string/join "|" db-concepts) ")")
                    file-graph-paths)]

+ 0 - 13
src/main/frontend/common/schema_register.clj

@@ -1,13 +0,0 @@
-(ns frontend.common.schema-register
-  "Macro 'defkeyword' to def keyword with docstring and malli-schema.
-   Used by frontend and worker namespaces")
-
-
-(defmacro defkeyword
-  "Define keyword with docstring and malli-schema"
-  [kw docstring & [optional-malli-schema]]
-  (assert (keyword? kw) "must be keyword")
-  (assert (some? docstring) "must have 'docstring' arg")
-  (when optional-malli-schema
-    `(do (assert (frontend.common.schema-register/not-register-yet? ~kw) (str "Already registered: " ~kw))
-         (frontend.common.schema-register/register! ~kw ~optional-malli-schema))))

+ 0 - 23
src/main/frontend/common/schema_register.cljs

@@ -1,23 +0,0 @@
-(ns frontend.common.schema-register
-  "Set malli default registry to a mutable one,
-  and use `register!` to add schemas dynamically."
-  (:require [malli.core :as m]
-            [malli.registry :as mr]))
-
-(def *malli-registry (atom {}))
-
-(defn register!
-  [type schema]
-  (swap! *malli-registry assoc type schema))
-
-(defn not-register-yet?
-  [type]
-  (boolean (nil? (@*malli-registry type))))
-
-(defn init
-  []
-  (reset! *malli-registry {})
-  (mr/set-default-registry!
-   (mr/composite-registry
-    (m/default-schemas)
-    (mr/mutable-registry *malli-registry))))

+ 10 - 48
src/main/frontend/common_keywords.cljs

@@ -1,50 +1,12 @@
 (ns frontend.common-keywords
   "There are some keywords scattered throughout the codebase."
-  (:require [frontend.common.schema-register :include-macros true :as sr]))
-
-(sr/defkeyword :block/uuid
-  "block's uuid"
-  :uuid)
-
-(sr/defkeyword :block/name
-  "block name, lowercase, only page-blocks have this attr"
-  :string)
-
-(sr/defkeyword :block/type
-  "block type"
-  [:enum "page" "property" "class" "whiteboard"])
-
-(sr/defkeyword :block/parent
-  "page blocks don't have this attr")
-
-(sr/defkeyword :block/order
-  "
-- page blocks don't have this attr
-- some no-order blocks don't have this attr too,
-  TODO: list these types")
-
-(sr/defkeyword :block/title
-  "Title or content string of the blocks.
-in db-version, page-references(e.g. [[page-name]]) are stored as [[uuid]]."
-  :string)
-
-(sr/defkeyword :block/raw-title
-  "like `:block/title`,
-but when eval `(:block/raw-title block-entity)`, return raw title of this block"
-  :string)
-
-(sr/defkeyword :kv/value
-  "Used to store key-value, the value could be anything, e.g. {:db/ident :logseq.kv/xxx :kv/value value}"
-  :any)
-
-(sr/defkeyword :logseq.property/parent
-  "A class's parent class")
-
-(sr/defkeyword :logseq.property.class/properties
-  "Class properties that all of its objects can use, notice that it's different from this class's own properties.")
-
-(sr/defkeyword :block/closed-value-property
-  "The property that this closed value (an Entity) belongs to.")
-
-(sr/defkeyword :property/schema.classes
-  "The classes that this property value must to sastify (being an object of a class)")
+  (:require [logseq.common.defkeywords :refer [defkeywords]]))
+
+(defkeywords
+  :block/uuid      {:doc "block's uuid"}
+  :block/name      {:doc "block name, lowercase, only page-blocks have this attr"}
+  :block/type      {:doc "block type, *deprecated* in db-version"}
+  :block/raw-title {:doc "like `:block/title`,
+                          but when eval `(:block/raw-title block-entity)`, return raw title of this block"}
+  :kv/value        {:doc "Used to store key-value, the value could be anything,
+                          e.g. {:db/ident :logseq.kv/xxx :kv/value value}"})

+ 5 - 4
src/main/frontend/components/all_pages.cljs

@@ -3,16 +3,17 @@
   (:require [frontend.components.block :as component-block]
             [frontend.components.page :as component-page]
             [frontend.components.views :as views]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.handler.page :as page-handler]
+            [frontend.hooks :as hooks]
             [frontend.state :as state]
+            [frontend.ui :as ui]
             [logseq.db :as ldb]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.ui :as ui]
-            [frontend.config :as config]))
+            [rum.core :as rum]))
 
 (defn- columns
   []
@@ -57,7 +58,7 @@
                                       {:with-object-name? false
                                        :with-id? false})
         view-entity (first (ldb/get-all-pages-views db))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js worker @state/*db-worker]
          (p/let [result-str (.get-page-refs-count worker (state/get-current-repo))

+ 16 - 17
src/main/frontend/components/assets.cljs

@@ -2,19 +2,20 @@
   (:require
    [clojure.set :refer [difference]]
    [clojure.string :as string]
-   [logseq.shui.ui :as shui]
-   [rum.core :as rum]
-   [frontend.state :as state]
-   [frontend.context.i18n :refer [t]]
-   [frontend.util :as util]
    [electron.ipc :as ipc]
-   [promesa.core :as p]
-   [medley.core :as medley]
-   [frontend.ui :as ui]
-   [frontend.config :as config]
    [frontend.components.select :as cp-select]
+   [frontend.config :as config]
+   [frontend.context.i18n :refer [t]]
+   [frontend.handler.assets :as assets-handler]
    [frontend.handler.notification :as notification]
-   [frontend.handler.assets :as assets-handler]))
+   [frontend.hooks :as hooks]
+   [frontend.state :as state]
+   [frontend.ui :as ui]
+   [frontend.util :as util]
+   [logseq.shui.ui :as shui]
+   [medley.core :as medley]
+   [promesa.core :as p]
+   [rum.core :as rum]))
 
 (defn -get-all-formats
   []
@@ -32,7 +33,7 @@
   (let [[*input-val, set-*input-val] (rum/use-state (atom nil))
         [input-empty?, set-input-empty?] (rum/use-state true)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(set-input-empty? (string/blank? @*input-val))
      [@*input-val])
 
@@ -80,7 +81,7 @@
                        (set-val! (util/trim-safe (.. e -target -value))))
         :on-key-up   (fn [^js e]
                        (when (and (= 13 (.-which e))
-                                (not (string/blank? val)))
+                                  (not (string/blank? val)))
                          (on-submit)))}]]
 
      [:div.pt-6.flex.justify-end
@@ -97,7 +98,7 @@
 
 (rum/defcs ^:large-vars/data-var alias-directories
   < rum/reactive
-    (rum/local nil ::ext-editing-dir)
+  (rum/local nil ::ext-editing-dir)
   [_state]
   (let [*ext-editing-dir (::ext-editing-dir _state)
         directories      (into [] (state/sub :assets/alias-dirs))
@@ -185,9 +186,7 @@
               (ui/icon "plus") "Acceptable file extensions"])]
 
           [:span.ctrls.flex.space-x-3.text-xs.opacity-30.hover:opacity-100.whitespace-nowrap.hidden.mt-1
-           [:a {:on-click #(rm-dir dir)} (ui/icon "trash-x")]]]
-
-         ])]
+           [:a {:on-click #(rm-dir dir)} (ui/icon "trash-x")]]]])]
 
      [:p.pt-2
       (ui/button
@@ -200,7 +199,7 @@
 
 (rum/defcs settings-content
   < rum/reactive
-    (rum/local (state/sub :assets/alias-enabled?) ::alias-enabled?)
+  (rum/local (state/sub :assets/alias-enabled?) ::alias-enabled?)
   [_state]
 
   (let [*pre-alias-enabled?    (::alias-enabled? _state)

+ 52 - 51
src/main/frontend/components/block.cljs

@@ -55,6 +55,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.intent :as mobile-intent]
             [frontend.mobile.util :as mobile-util]
@@ -273,13 +274,13 @@
   (let [handle-props {}
         add-resizing-class! #(dom/add-class! js/document.documentElement "is-resizing-buf")
         remove-resizing-class! #(dom/remove-class! js/document.documentElement "is-resizing-buf")
-        *handle-left (rum/use-ref nil)
-        *handle-right (rum/use-ref nil)]
+        *handle-left (hooks/use-ref nil)
+        *handle-right (hooks/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
-       (doseq [el [(rum/deref *handle-left)
-                   (rum/deref *handle-right)]]
+       (doseq [el [(hooks/deref *handle-left)
+                   (hooks/deref *handle-right)]]
          (-> (js/interact el)
              (.draggable
               (bean/->js
@@ -767,12 +768,12 @@
 
 (rum/defc popup-preview-impl
   [children {:keys [*timer *timer1 visible? set-visible! render *el-popup]}]
-  (let [*el-trigger (rum/use-ref nil)]
-    (rum/use-effect!
+  (let [*el-trigger (hooks/use-ref nil)]
+    (hooks/use-effect!
      (fn []
        (when (true? visible?)
          (shui/popup-show!
-          (rum/deref *el-trigger) render
+          (hooks/deref *el-trigger) render
           {:root-props {:onOpenChange (fn [v] (set-visible! v))
                         :modal false}
            :content-props {:class "ls-preview-popup"
@@ -780,15 +781,15 @@
                            :onEscapeKeyDown (fn [^js e]
                                               (when (state/editing?)
                                                 (.preventDefault e)
-                                                (some-> (rum/deref *el-popup) (.focus))))}
+                                                (some-> (hooks/deref *el-popup) (.focus))))}
            :as-dropdown? false}))
 
        (when (false? visible?)
          (shui/popup-hide!)
          (when (state/get-edit-block)
            (state/clear-edit!)))
-       (rum/set-ref! *timer nil)
-       (rum/set-ref! *timer1 nil)
+       (hooks/set-ref! *timer nil)
+       (hooks/set-ref! *timer1 nil)
         ;; teardown
        (fn []
          (when visible?
@@ -799,42 +800,42 @@
      {:ref *el-trigger
       :on-mouse-enter (fn [^js e]
                         (when (= (some-> (.-target e) (.closest ".preview-ref-link"))
-                                 (rum/deref *el-trigger))
-                          (let [timer (rum/deref *timer)
-                                timer1 (rum/deref *timer1)]
+                                 (hooks/deref *el-trigger))
+                          (let [timer (hooks/deref *timer)
+                                timer1 (hooks/deref *timer1)]
                             (when-not timer
-                              (rum/set-ref! *timer
-                                            (js/setTimeout #(set-visible! true) 1000)))
+                              (hooks/set-ref! *timer
+                                              (js/setTimeout #(set-visible! true) 1000)))
                             (when timer1
                               (js/clearTimeout timer1)
-                              (rum/set-ref! *timer1 nil)))))
+                              (hooks/set-ref! *timer1 nil)))))
       :on-mouse-leave (fn []
-                        (let [timer (rum/deref *timer)
-                              timer1 (rum/deref *timer1)]
+                        (let [timer (hooks/deref *timer)
+                              timer1 (hooks/deref *timer1)]
                           (when (or (number? timer) (number? timer1))
                             (when timer
                               (js/clearTimeout timer)
-                              (rum/set-ref! *timer nil))
+                              (hooks/set-ref! *timer nil))
                             (when-not timer1
-                              (rum/set-ref! *timer1
-                                            (js/setTimeout #(set-visible! false) 300))))))}
+                              (hooks/set-ref! *timer1
+                                              (js/setTimeout #(set-visible! false) 300))))))}
      children]))
 
 (rum/defc page-preview-trigger
   [{:keys [children sidebar? open? manual?] :as config} page-entity]
-  (let [*timer (rum/use-ref nil)                            ;; show
-        *timer1 (rum/use-ref nil)                           ;; hide
-        *el-popup (rum/use-ref nil)
-        *el-wrap (rum/use-ref nil)
+  (let [*timer (hooks/use-ref nil)                            ;; show
+        *timer1 (hooks/use-ref nil)                           ;; hide
+        *el-popup (hooks/use-ref nil)
+        *el-wrap (hooks/use-ref nil)
         [in-popup? set-in-popup!] (rum/use-state nil)
         [visible? set-visible!] (rum/use-state nil)
         ;; set-visible! (fn debug-visible [v] (js/console.warn "debug: visible" v) (set-visible! v))
         _  #_:clj-kondo/ignore (rum/defc preview-render []
                                  (let [[ready? set-ready!] (rum/use-state false)]
 
-                                   (rum/use-effect!
+                                   (hooks/use-effect!
                                     (fn []
-                                      (let [el-popup (rum/deref *el-popup)
+                                      (let [el-popup (hooks/deref *el-popup)
                                             focus! #(js/setTimeout (fn [] (.focus el-popup)))]
                                         (set-ready! true)
                                         (focus!)
@@ -851,23 +852,23 @@
                                                :font-weight 500
                                                :padding-bottom 64}
                                        :on-mouse-enter (fn []
-                                                         (when-let [timer1 (rum/deref *timer1)]
+                                                         (when-let [timer1 (hooks/deref *timer1)]
                                                            (js/clearTimeout timer1)))
                                        :on-mouse-leave (fn []
                                                          ;; check the top popup whether is the preview popup
                                                          (when (ui/last-shui-preview-popup?)
-                                                           (rum/set-ref! *timer1
-                                                                         (js/setTimeout #(set-visible! false) 500))))}
+                                                           (hooks/set-ref! *timer1
+                                                                           (js/setTimeout #(set-visible! false) 500))))}
                                       (when-let [page-cp (and ready? (state/get-page-blocks-cp))]
                                         (page-cp {:repo (state/get-current-repo)
                                                   :page-name (str (:block/uuid source))
                                                   :sidebar? sidebar?
-                                                  :scroll-container (some-> (rum/deref *el-popup) (.closest ".ls-preview-popup"))
+                                                  :scroll-container (some-> (hooks/deref *el-popup) (.closest ".ls-preview-popup"))
                                                   :preview? true}))])))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
-       (if (some-> (rum/deref *el-wrap) (.closest "[data-radix-popper-content-wrapper]"))
+       (if (some-> (hooks/deref *el-wrap) (.closest "[data-radix-popper-content-wrapper]"))
          (set-in-popup! true)
          (set-in-popup! false)))
      [])
@@ -1193,8 +1194,8 @@
 
 (rum/defc block-reference-preview
   [children {:keys [repo config id]}]
-  (let [*timer (rum/use-ref nil)                            ;; show
-        *timer1 (rum/use-ref nil)                           ;; hide
+  (let [*timer (hooks/use-ref nil)                            ;; show
+        *timer1 (hooks/use-ref nil)                           ;; hide
         [visible? set-visible!] (rum/use-state nil)
         _ #_:clj-kondo/ignore (rum/defc render []
                                 [:div.tippy-wrapper.as-block
@@ -1202,13 +1203,13 @@
                                           :font-weight 500
                                           :text-align "left"}
                                   :on-mouse-enter (fn []
-                                                    (when-let [timer1 (rum/deref *timer1)]
+                                                    (when-let [timer1 (hooks/deref *timer1)]
                                                       (js/clearTimeout timer1)))
 
                                   :on-mouse-leave (fn []
                                                     (when (ui/last-shui-preview-popup?)
-                                                      (rum/set-ref! *timer1
-                                                                    (js/setTimeout #(set-visible! false) 500))))}
+                                                      (hooks/set-ref! *timer1
+                                                                      (js/setTimeout #(set-visible! false) 500))))}
                                  [(breadcrumb config repo id {:indent? true})
                                   (blocks-container
                                    (assoc config :id (str id) :preview? true)
@@ -2750,7 +2751,7 @@
     (let [[result set-result!] (rum/use-state nil)
           repo (state/get-current-repo)
           [status-history time-spent] result]
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (p/let [result (db-async/<task-spent-time repo (:db/id block))]
            (set-result! result)))
@@ -3820,8 +3821,8 @@
   [config options]
   (let [block (or (:code-block config) (:block config))
         container-id (:container-id config)
-        *mode-ref (rum/use-ref nil)
-        *actions-ref (rum/use-ref nil)]
+        *mode-ref (hooks/use-ref nil)
+        *actions-ref (hooks/use-ref nil)]
 
     (when options
       (let [html-export? (:html-export? config)
@@ -3839,10 +3840,10 @@
             [:div.ui-fenced-code-editor.flex.w-full
              {: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-over #(dom/add-class! (hooks/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")))}
+                                  (dom/remove-class! (hooks/deref *actions-ref) "opacity-100")))}
              (cond
                (nil? inside-portal?) nil
 
@@ -4123,7 +4124,7 @@
                                     {:top? top?
                                      :bottom? bottom?})))
         virtualized? (and virtualized? (seq blocks))
-        *virtualized-ref (rum/use-ref nil)
+        *virtualized-ref (hooks/use-ref nil)
         virtual-opts (when virtualized?
                        {:ref *virtualized-ref
                         :custom-scroll-parent (or (:scroll-container config)
@@ -4143,8 +4144,8 @@
                                                       block
                                                       {:top? top?
                                                        :bottom? bottom?})))})
-        *wrap-ref (rum/use-ref nil)]
-    (rum/use-effect!
+        *wrap-ref (hooks/use-ref nil)]
+    (hooks/use-effect!
      (fn []
        (when virtualized?
          (when (:current-page? config)
@@ -4156,13 +4157,13 @@
          (let [^js *ob (volatile! nil)]
            (js/setTimeout
             (fn []
-              (when-let [_inst (rum/deref *virtualized-ref)]
-                (when-let [^js target (.-firstElementChild (rum/deref *wrap-ref))]
-                  (let [set-wrap-h! #(when-let [ref (rum/deref *wrap-ref)] (set! (.-height (.-style ref)) %))
+              (when-let [_inst (hooks/deref *virtualized-ref)]
+                (when-let [^js target (.-firstElementChild (hooks/deref *wrap-ref))]
+                  (let [set-wrap-h! #(when-let [ref (hooks/deref *wrap-ref)] (set! (.-height (.-style ref)) %))
                         set-wrap-h! (debounce set-wrap-h! 16)
                         ob (js/ResizeObserver.
                             (fn []
-                              (when-let [h (and (rum/deref *wrap-ref)
+                              (when-let [h (and (hooks/deref *wrap-ref)
                                                 (.-height (.-style target)))]
                                    ;(prn "==>> debug: " h)
                                 (set-wrap-h! h))))]

+ 15 - 14
src/main/frontend/components/bug_report.cljs

@@ -1,12 +1,13 @@
 (ns frontend.components.bug-report
-  (:require [rum.core :as rum]
-            [frontend.ui :as ui]
+  (:require [clojure.string :as string]
             [frontend.components.header :as header]
+            [frontend.context.i18n :refer [t]]
+            [frontend.handler.notification :as notification]
+            [frontend.hooks :as hooks]
+            [frontend.ui :as ui]
             [frontend.util :as util]
             [reitit.frontend.easy :as rfe]
-            [clojure.string :as string]
-            [frontend.handler.notification :as notification]
-            [frontend.context.i18n :refer [t]]))
+            [rum.core :as rum]))
 
 (defn parse-clipboard-data-transfer
   "parse dataTransfer
@@ -49,7 +50,7 @@
                       (set-step! 0)
                       (set-result! {}))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (cond (= step 0) (js/addEventListener "paste" paste-handler!))
        (fn [] (cond (= step 0) (js/removeEventListener "paste" paste-handler!))))
@@ -93,11 +94,11 @@
 
 (rum/defc report-item-button
   [title description icon-name {:keys [on-click]}]
-   [:a.cp__bug-report-item-button.flex.items-center.px-4.py-2.my-2.rounded-lg {:on-click on-click}
-    [(ui/icon icon-name)
-     [:div.flex.flex-col.ml-2
-      [:div title]
-      [:div.opacity-60 description]]]])
+  [:a.cp__bug-report-item-button.flex.items-center.px-4.py-2.my-2.rounded-lg {:on-click on-click}
+   [(ui/icon icon-name)
+    [:div.flex.flex-col.ml-2
+     [:div title]
+     [:div.opacity-60 description]]]])
 
 (rum/defc bug-report
   []
@@ -111,9 +112,9 @@
     [:h1.text-2xl (t :bug-report/section-clipboard-title)]
     [:div.opacity-60 (t :bug-report/section-clipboard-desc)]
     (report-item-button (t :bug-report/section-clipboard-btn-title)
-                 (t :bug-report/section-clipboard-btn-desc)
-                 "clipboard"
-                 {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
+                        (t :bug-report/section-clipboard-btn-desc)
+                        "clipboard"
+                        {:on-click #(util/open-url (rfe/href :bug-report-tools {:tool "clipboard-data-inspector"}))})
     [:div.py-2] ;; divider
     [:div.flex.flex-col
      [:h1.text-2xl (t :bug-report/section-issues-title)]

+ 8 - 7
src/main/frontend/components/cmdk/core.cljs

@@ -18,6 +18,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.utils :as shortcut-utils]
@@ -549,7 +550,7 @@
 
 (rum/defc mouse-active-effect!
   [*mouse-active? deps]
-  (rum/use-effect!
+  (hooks/use-effect!
    #(reset! *mouse-active? false)
    deps)
   nil)
@@ -778,7 +779,7 @@
   (let [highlighted-item @(::highlighted-item state)
         input @(::input state)
         input-ref (::input-ref state)
-        debounced-on-change (rum/use-callback
+        debounced-on-change (hooks/use-callback
                              (gfun/debounce
                               (fn [e]
                                 (let [new-value (.-value (.-target e))]
@@ -790,11 +791,11 @@
     ;; use-effect [results-ordered input] to check whether the highlighted item is still in the results,
     ;; if not then clear that puppy out!
     ;; This was moved to a functional component
-    (rum/use-effect! (fn []
-                       (when (and highlighted-item (= -1 (.indexOf all-items (dissoc highlighted-item :mouse-enter-triggered-highlight))))
-                         (reset! (::highlighted-item state) nil)))
-                     [all-items])
-    (rum/use-effect! (fn [] (load-results :default state)) [])
+    (hooks/use-effect! (fn []
+                         (when (and highlighted-item (= -1 (.indexOf all-items (dissoc highlighted-item :mouse-enter-triggered-highlight))))
+                           (reset! (::highlighted-item state) nil)))
+                       [all-items])
+    (hooks/use-effect! (fn [] (load-results :default state)) [])
     [:div {:class "bg-gray-02 border-b border-1 border-gray-07"}
      [:input.cp__cmdk-search-input
       {:class "text-xl bg-transparent border-none w-full outline-none px-3 py-3"

+ 31 - 30
src/main/frontend/components/cmdk/list_item.cljs

@@ -1,10 +1,11 @@
 (ns frontend.components.cmdk.list-item
   (:require
    ["remove-accents" :as remove-accents]
-   [rum.core :as rum]
    [clojure.string :as string]
+   [frontend.hooks :as hooks]
    [goog.string :as gstring]
-   [logseq.shui.ui :as shui]))
+   [logseq.shui.ui :as shui]
+   [rum.core :as rum]))
 
 (defn- to-string [input]
   (cond
@@ -40,11 +41,11 @@
             segs (string/split highlighted-text #"<:hlmarker>")]
         (if (seq segs)
           (into [:span]
-            (map-indexed (fn [i seg]
-                           (if (even? i)
-                             [:span seg]
-                             [:span {:class "ui__list-item-highlighted-span"} seg]))
-              segs))
+                (map-indexed (fn [i seg]
+                               (if (even? i)
+                                 [:span seg]
+                                 [:span {:class "ui__list-item-highlighted-span"} seg]))
+                             segs))
           [:span normal-text])))))
 
 (rum/defc root [{:keys [group icon icon-theme query text info shortcut value-label value
@@ -52,29 +53,29 @@
                         hoverable compact rounded on-mouse-enter component-opts source-page] :as _props
                  :or {hoverable true rounded true}}
                 {:keys [app-config]}]
-  (let [ref (rum/create-ref)
+  (let [ref (hooks/create-ref)
         highlight-query (partial highlight-query* app-config query)
         [hover? set-hover?] (rum/use-state false)]
-    (rum/use-effect!
-      (fn []
-        (when (and highlighted on-highlight)
-          (on-highlight ref)))
-      [highlighted on-highlight-dep])
+    (hooks/use-effect!
+     (fn []
+       (when (and highlighted on-highlight)
+         (on-highlight ref)))
+     [highlighted on-highlight-dep])
     [:div (merge
-            {:style {:opacity (if highlighted 1 0.8)}
-             :class (cond-> "flex flex-col transition-opacity"
-                      highlighted (str " !opacity-100 bg-gray-03-alpha dark:bg-gray-04-alpha")
-                      hoverable (str " transition-all duration-50 ease-in !opacity-75 hover:!opacity-100 hover:cursor-pointer hover:bg-gradient-to-r hover:from-gray-03-alpha hover:to-gray-01-alpha from-0% to-100%")
-                      (and hoverable rounded) (str " !rounded-lg")
-                      (not compact) (str " py-4 px-6 gap-1")
-                      compact (str " py-1.5 px-3 gap-0.5")
-                      (not highlighted) (str " "))
-             :ref ref
-             :on-click (when on-click on-click)
-             :on-mouse-over #(set-hover? true)
-             :on-mouse-out #(set-hover? false)
-             :on-mouse-enter (when on-mouse-enter on-mouse-enter)}
-            component-opts)
+           {:style {:opacity (if highlighted 1 0.8)}
+            :class (cond-> "flex flex-col transition-opacity"
+                     highlighted (str " !opacity-100 bg-gray-03-alpha dark:bg-gray-04-alpha")
+                     hoverable (str " transition-all duration-50 ease-in !opacity-75 hover:!opacity-100 hover:cursor-pointer hover:bg-gradient-to-r hover:from-gray-03-alpha hover:to-gray-01-alpha from-0% to-100%")
+                     (and hoverable rounded) (str " !rounded-lg")
+                     (not compact) (str " py-4 px-6 gap-1")
+                     compact (str " py-1.5 px-3 gap-0.5")
+                     (not highlighted) (str " "))
+            :ref ref
+            :on-click (when on-click on-click)
+            :on-mouse-over #(set-hover? true)
+            :on-mouse-out #(set-hover? false)
+            :on-mouse-enter (when on-mouse-enter on-mouse-enter)}
+           component-opts)
      ;; header
      (when header
        [:div.text-xs.pl-8.font-light {:class "-mt-1"
@@ -87,9 +88,9 @@
                 :box-shadow (when (#{:gradient} icon-theme) "inset 0 0 0 1px rgba(255,255,255,0.3) ")}
         :class (cond-> "w-5 h-5 rounded flex items-center justify-center"
                  (= icon-theme :color) (str
-                                         " "
-                                         (if highlighted "bg-accent-07-alpha" "bg-gray-05")
-                                         " dark:text-white")
+                                        " "
+                                        (if highlighted "bg-accent-07-alpha" "bg-gray-05")
+                                        " dark:text-white")
                  (= icon-theme :gray) (str " bg-gray-05 dark:text-white"))}
        (shui/tabler-icon icon {:size "14" :class ""})]
       [:div.flex.flex-1.flex-col

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

@@ -31,6 +31,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.action-bar :as action-bar]
             [frontend.mobile.footer :as footer]
@@ -198,7 +199,7 @@
   [{:keys [_id navs checked-navs set-checked-navs!]}]
   (let [[local-navs set-local-navs!] (rum/use-state checked-navs)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (set-checked-navs! local-navs))
      [local-navs])
@@ -225,7 +226,7 @@
         [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations)
                                                             [:whiteboards :flashcards :graph-view :all-pages]))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (vector? checked-navs)
          (storage/set :ls-sidebar-navigations checked-navs)))
@@ -414,7 +415,7 @@
                           (some->> (:width el-rect)
                                    (/ touching-x-offset)))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(js/setTimeout
        (fn [] (some-> (rum/deref ref-el)
                       (.getBoundingClientRect)
@@ -424,7 +425,7 @@
        16)
      [])
 
-    (rum/use-layout-effect!
+    (hooks/use-layout-effect!
      (fn []
        (when (and (rum/deref ref-open?) local-closing?)
          (reset! *closing? true))
@@ -432,7 +433,7 @@
        #())
      [local-closing? left-sidebar-open?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-not (neg? close-signal)
          (close-fn)))
@@ -498,14 +499,13 @@
                        (storage/set :ls-left-sidebar-width width))]
 
     ;; restore size
-    (rum/use-layout-effect!
+    (hooks/use-layout-effect!
      (fn []
        (when-let [width (storage/get :ls-left-sidebar-width)]
-         (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)))
-     [])
+         (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width))))
 
     ;; draggable handler
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
          (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
@@ -759,7 +759,7 @@
 (rum/defc render-custom-context-menu
   [links position]
   (let [ref (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      #(let [el (rum/deref ref)
             {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
         (set! (.. el -style -transform)
@@ -814,12 +814,12 @@
 (rum/defc help-menu-popup
   []
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (state/set-state! :ui/handbooks-open? false))
    [])
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [h #(state/set-state! :ui/help-open? false)]
        (.addEventListener js/document.body "click" h)

+ 6 - 5
src/main/frontend/components/dnd.cljs

@@ -1,11 +1,12 @@
 (ns frontend.components.dnd
-  (:require [rum.core :as rum]
-            [cljs-bean.core :as bean]
+  (:require ["@dnd-kit/core" :refer [DndContext closestCenter MouseSensor useSensor useSensors]]
             ["@dnd-kit/sortable" :refer [useSortable arrayMove SortableContext verticalListSortingStrategy horizontalListSortingStrategy] :as sortable]
             ["@dnd-kit/utilities" :refer [CSS]]
-            ["@dnd-kit/core" :refer [DndContext closestCenter MouseSensor useSensor useSensors]]
+            [cljs-bean.core :as bean]
+            [frontend.hooks :as hooks]
             [frontend.rum :as r]
-            [frontend.state :as state]))
+            [frontend.state :as state]
+            [rum.core :as rum]))
 
 (def dnd-context (r/adapt-class DndContext))
 (def sortable-context (r/adapt-class SortableContext))
@@ -40,7 +41,7 @@
         items' (bean/->js ids)
         id->item (zipmap ids col)
         [items-state set-items] (rum/use-state items')
-        _ (rum/use-effect! (fn [] (set-items items')) [col])
+        _ (hooks/use-effect! (fn [] (set-items items')) [col])
         [_active-id set-active-id] (rum/use-state nil)
         sensors (useSensors (useSensor MouseSensor (bean/->js {:activationConstraint {:distance 8}})))
         dnd-opts {:sensors sensors

+ 11 - 9
src/main/frontend/components/editor.cljs

@@ -19,6 +19,7 @@
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.search :as search-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.search :refer [fuzzy-search]]
             [frontend.state :as state]
@@ -146,7 +147,8 @@
                                       (editor-handler/get-matched-classes q)
                                       (editor-handler/<get-matched-blocks q {:nlp-pages? true}))]
                        (set-matched-pages! result))))]
-    (rum/use-effect! search-f [(mixins/use-debounce 50 q)])
+    (hooks/use-effect! search-f [(hooks/use-debounced-value q 50)])
+
     (let [matched-pages' (if (string/blank? q)
                            (if db-tag?
                              (db-model/get-all-classes (state/get-current-repo) {:except-root-class? true})
@@ -341,10 +343,10 @@
 (rum/defc template-search-aux
   [id q]
   (let [[matched-templates set-matched-templates!] (rum/use-state nil)]
-    (rum/use-effect! (fn []
-                       (p/let [result (editor-handler/<get-matched-templates q)]
-                         (set-matched-templates! result)))
-                     [q])
+    (hooks/use-effect! (fn []
+                         (p/let [result (editor-handler/<get-matched-templates q)]
+                           (set-matched-templates! result)))
+                       [q])
     (ui/auto-complete
      matched-templates
      {:on-chosen   (editor-handler/template-on-chosen-handler id)
@@ -374,7 +376,7 @@
     (when input
       (let [q (or (:searching-property (editor-handler/get-searching-property input))
                   "")]
-        (rum/use-effect!
+        (hooks/use-effect!
          (fn []
            (p/let [matched-properties (editor-handler/<get-matched-properties q)]
              (set-matched-properties! matched-properties)))
@@ -394,7 +396,7 @@
 (rum/defc property-value-search-aux
   [id property q]
   (let [[values set-values!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/let [result (editor-handler/get-matched-property-values property q)]
          (set-values! result)))
@@ -428,7 +430,7 @@
 
 (rum/defc code-block-mode-keyup-listener
   [_q _edit-content last-pos current-pos]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (when (< current-pos last-pos)
        (state/clear-editor-action!)))
@@ -648,7 +650,7 @@
 
 (rum/defc shui-editor-popups
   [id format action _data]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [pid (case action
                  :commands

+ 4 - 3
src/main/frontend/components/file_based/block.cljs

@@ -4,12 +4,13 @@
             [frontend.components.file-based.datetime :as datetime-comp]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file-based.repeated :as repeated]
+            [frontend.hooks :as hooks]
+            [frontend.state :as state]
             [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]
-            [frontend.state :as state]
+            [logseq.shui.ui :as shui]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]))
 
@@ -115,7 +116,7 @@
 (rum/defc timestamp-editor
   [ast *show-datapicker?]
   (let [*trigger-ref (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [pid (shui/popup-show!
                   (.closest (rum/deref *trigger-ref) "a")

+ 18 - 17
src/main/frontend/components/file_sync.cljs

@@ -1,7 +1,8 @@
 (ns frontend.components.file-sync
-  (:require [cljs.core.async :as async]
+  (:require [cljs-time.coerce :as tc]
+            [cljs-time.core :as t]
+            [cljs.core.async :as async]
             [cljs.core.async.interop :refer [p->c]]
-            [frontend.util.persist-var :as persist-var]
             [clojure.string :as string]
             [electron.ipc :as ipc]
             [frontend.components.lazy-editor :as lazy-editor]
@@ -12,26 +13,26 @@
             [frontend.db.model :as db-model]
             [frontend.fs :as fs]
             [frontend.fs.sync :as fs-sync]
+            [frontend.handler.file-based.nfs :as nfs-handler]
             [frontend.handler.file-sync :refer [*beta-unavailable?] :as file-sync-handler]
             [frontend.handler.notification :as notification]
+            [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.user :as user-handler]
-            [frontend.handler.page :as page-handler]
-            [frontend.handler.file-based.nfs :as nfs-handler]
+            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
+            [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.util.fs :as fs-util]
-            [frontend.storage :as storage]
+            [frontend.util.persist-var :as persist-var]
+            [goog.functions :refer [debounce]]
+            [logseq.common.util :as common-util]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [reitit.frontend.easy :as rfe]
-            [rum.core :as rum]
-            [cljs-time.core :as t]
-            [cljs-time.coerce :as tc]
-            [goog.functions :refer [debounce]]
-            [logseq.common.util :as common-util]))
+            [rum.core :as rum]))
 
 (declare maybe-onboarding-show)
 (declare open-icloud-graph-clone-picker)
@@ -39,7 +40,7 @@
 (rum/defc clone-local-icloud-graph-panel
   [repo graph-name close-fn]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    #(some->> (state/sub :file-sync/jstour-inst)
              (.complete))
    [])
@@ -120,7 +121,7 @@
 (rum/defc create-remote-graph-panel
   [repo graph-name close-fn]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    #(some->> (state/sub :file-sync/jstour-inst)
              (.complete))
    [])
@@ -194,7 +195,7 @@
   [sync-state sync-progress
    {:keys [idle? syncing? no-active-files? online? history-files? queuing?]}]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      #(reset! *last-calculated-time nil))
    [])
@@ -241,7 +242,7 @@
                                           (-> (storage/get :ui/file-sync-active-file-list?)
                                               (#(if (nil? %) true %))))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js outer-class-list
                   (some-> (rum/deref *el-ref)
@@ -575,7 +576,7 @@
         get-version-key #(or (:VersionUUID %) (:relative-path %))]
 
     ;; fetch version files
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-not loading?
          (async/go
@@ -623,7 +624,7 @@
         *ref-contents      (rum/use-ref (atom {}))
         original-page-name (or (:block/title page-entity) page-name)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when selected-page
         (set-content-ready? false)
         (let [k               (get-version-key selected-page)
@@ -653,7 +654,7 @@
                   (load-file' repo-url relative-path)))))))
      [selected-page])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (state/update-state! :editor/hidden-editors #(conj % page-name))
 

+ 8 - 7
src/main/frontend/components/git.cljs

@@ -1,12 +1,13 @@
 (ns frontend.components.git
-  (:require [rum.core :as rum]
+  (:require [clojure.string :as string]
+            [frontend.handler.file :as file]
+            [frontend.handler.shell :as shell]
+            [frontend.hooks :as hooks]
+            [frontend.state :as state]
             [frontend.ui :as ui]
-            [promesa.core :as p]
             [frontend.util :as util]
-            [clojure.string :as string]
-            [frontend.handler.shell :as shell]
-            [frontend.handler.file :as file]
-            [frontend.state :as state]))
+            [promesa.core :as p]
+            [rum.core :as rum]))
 
 (rum/defcs set-git-username-and-email <
   (rum/local "" ::username)
@@ -50,7 +51,7 @@
   (let
    [[content set-content!] (rum/use-state  nil)
     [hash  set-hash!] (rum/use-state   "HEAD")]
-    (rum/use-effect! (fn [] (p/let [c (get-content hash path)] (set-content! c)) [hash path]))
+    (hooks/use-effect! (fn [] (p/let [c (get-content hash path)] (set-content! c)) [hash path]))
     [:div.flex
      [:div.overflow-y-auto {:class "w-48 max-h-[calc(85vh_-_4rem)] "}
       [:div.font-bold "File history - " path]

+ 8 - 7
src/main/frontend/components/handbooks.cljs

@@ -1,9 +1,10 @@
 (ns frontend.components.handbooks
-  (:require [rum.core :as rum]
-            [frontend.state :as state]
-            [frontend.modules.layout.core :as layout]
-            ;[shadow.lazy :as lazy]
-            [frontend.extensions.handbooks.core :as handbooks]))
+  (:require ;[shadow.lazy :as lazy]
+   [frontend.extensions.handbooks.core :as handbooks]
+   [frontend.hooks :as hooks]
+   [frontend.modules.layout.core :as layout]
+   [frontend.state :as state]
+   [rum.core :as rum]))
 
 #_:clj-kondo/ignore
 ;(def lazy-handbooks (lazy/loadable frontend.extensions.handbooks.core/content))
@@ -12,7 +13,7 @@
 ;  []
 ;  (let [[content set-content] (rum/use-state nil)]
 ;
-;    (rum/use-effect!
+;    (hooks/use-effect!
 ;     (fn []
 ;       (lazy/load lazy-handbooks #(set-content %))) [])
 ;
@@ -22,7 +23,7 @@
 (rum/defc handbooks-popup
   []
   (let [popup-ref (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js popup-el (rum/deref popup-ref)]
          (comp

+ 5 - 4
src/main/frontend/components/header.cljs

@@ -4,6 +4,7 @@
             [cljs-time.core :as t]
             [clojure.string :as string]
             [dommy.core :as d]
+            [frontend.common.missionary :as c.m]
             [frontend.components.export :as export]
             [frontend.components.file-sync :as fs-sync]
             [frontend.components.page-menu :as page-menu]
@@ -22,13 +23,13 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.state :as state]
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [frontend.version :refer [version]]
-            [frontend.common.missionary :as c.m]
             [logseq.db :as ldb]
             [logseq.shui.ui :as shui]
             [logseq.shui.util :as shui-util]
@@ -234,7 +235,7 @@
 (rum/defc updater-tips-new-version
   [t]
   (let [[downloaded, set-downloaded] (rum/use-state nil)
-        _ (rum/use-effect!
+        _ (hooks/use-effect!
            (fn []
              (when-let [channel (and (util/electron?) "auto-updater-downloaded")]
                (let [callback (fn [_ args]
@@ -265,12 +266,12 @@
   []
   (let [[recent-days set-recent-days!] (rum/use-state (state/get-highlight-recent-days))
         [thumb-ref set-thumb-ref!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when thumb-ref
          (.focus ^js thumb-ref)))
      [thumb-ref])
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [all-nodes (d/by-class "ls-block")
              recent-node (fn [node]

+ 15 - 14
src/main/frontend/components/icon.cljs

@@ -1,22 +1,23 @@
 (ns frontend.components.icon
   (:require ["@emoji-mart/data" :as emoji-data]
             ["emoji-mart" :refer [SearchIndex]]
-            [promesa.core :as p]
-            [cljs-bean.core :as bean]
             [camel-snake-kebab.core :as csk]
+            [cljs-bean.core :as bean]
             [clojure.string :as string]
+            [frontend.config :as config]
+            [frontend.handler.property.util :as pu]
+            [frontend.hooks :as hooks]
             [frontend.search :as search]
             [frontend.storage :as storage]
-            [medley.core :as medley]
-            [rum.core :as rum]
             [frontend.ui :as ui]
-            [logseq.shui.ui :as shui]
             [frontend.util :as util]
-            [goog.object :as gobj]
             [goog.functions :refer [debounce]]
-            [frontend.config :as config]
-            [frontend.handler.property.util :as pu]
-            [logseq.db :as ldb]))
+            [goog.object :as gobj]
+            [logseq.db :as ldb]
+            [logseq.shui.ui :as shui]
+            [medley.core :as medley]
+            [promesa.core :as p]
+            [rum.core :as rum]))
 
 (defonce emojis (vals (bean/->clj (gobj/get emoji-data "emojis"))))
 
@@ -227,7 +228,7 @@
 
 (rum/defc tab-observer
   [tab {:keys [reset-q!]}]
-  (rum/use-effect!
+  (hooks/use-effect!
    #(reset-q!)
    [tab])
   nil)
@@ -252,7 +253,7 @@
                          (set-current! idx node))
                      (do (.focus (rum/deref *input-ref)) (set-current! -1 nil)))))
         down-handler!
-        (rum/use-callback
+        (hooks/use-callback
          (fn [^js e]
            (let []
              (if (= 13 (.-keyCode e))
@@ -270,7 +271,7 @@
                    40 (do (focus! (+ idx 9) :next) (util/stop e))
                    :dune))))) [])]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
         ;; calculate items
        (let [^js sections (.querySelectorAll (get-cnt) ".pane-section")
@@ -305,7 +306,7 @@
                             :size :sm :variant :outline
                             :class "it" :style {:background-color c}}
                            (if c "" (shui/tabler-icon "minus" {:class "scale-75 opacity-70"}))))]))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js picker (some-> (rum/deref *el) (.closest ".cp__emoji-icon-picker"))]
          (let [color (if (string/blank? color) "inherit" color)]
@@ -453,7 +454,7 @@
                            (when-not (true? keep-popup?) (shui/popup-hide! id)))
               :icon-value icon-value
               :del-btn? del-btn?})))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when initial-open?
          (js/setTimeout #(some-> (rum/deref *trigger-ref) (.click)) 32)))

+ 2 - 1
src/main/frontend/components/imports.cljs

@@ -16,6 +16,7 @@
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.hooks :as hooks]
             [frontend.persist-db.browser :as db-browser]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -429,7 +430,7 @@
 
 (rum/defc import-indicator
   [importing?]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (when (and importing? (not (shui-dialog/get-modal :import-indicator)))
        (shui/dialog-open! indicator-progress

+ 3 - 2
src/main/frontend/components/objects.cljs

@@ -9,6 +9,7 @@
             [frontend.db.model :as db-model]
             [frontend.db.react :as react]
             [frontend.handler.editor :as editor-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
@@ -134,7 +135,7 @@
                     (concat before-cols [(build-asset-file-column config)] after-cols))
                   columns)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (nil? loading?)
          (set-loading? true)
@@ -230,7 +231,7 @@
         [data set-data!] (rum/use-state objects)
         columns (views/build-columns config properties)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (set-loading? true)
        (p/let [_result (db-async/<get-views (state/get-current-repo) (:db/id property))

+ 6 - 5
src/main/frontend/components/page.cljs

@@ -33,6 +33,7 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.util :as mobile-util]
             [frontend.rum :as frontend-rum]
@@ -140,9 +141,9 @@
           focus! (fn [] (js/setTimeout #(some-> (rum/deref *el-ref) (.focus)) 16))]
 
       ;; mounted
-      ;(rum/use-effect! #(focus!) [])
-      (rum/use-effect! #(if selected? (focus!)
-                            (some-> (rum/deref *el-ref) (.blur))) [selected?])
+      ;(hooks/use-effect! #(focus!) [])
+      (hooks/use-effect! #(if selected? (focus!)
+                              (some-> (rum/deref *el-ref) (.blur))) [selected?])
 
       (shui/trigger-as
        :div.ls-dummy-block.ls-block
@@ -273,7 +274,7 @@
 (rum/defc tagged-pages
   [repo tag tag-title]
   (let [[pages set-pages!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/let [result (db-async/<get-tag-pages repo (:db/id tag))]
          (set-pages! result)))
@@ -453,7 +454,7 @@
   (let [[with-actions? set-with-actions!] (rum/use-state false)
         *el (rum/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (and (not config/publishing?)
                   (some-> (rum/deref *el) (.closest "#main-content-container")))

+ 67 - 31
src/main/frontend/components/plugins.cljs

@@ -12,6 +12,7 @@
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.rum :as rum-utils]
             [frontend.search :as search]
@@ -134,7 +135,7 @@
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [err-handle
            (fn [^js e]
@@ -404,14 +405,14 @@
    :intent "link"
    :target "_blank"))
 
-(rum/defc user-proxy-settings-panel
+(rum/defc user-proxy-settings-container
   [{:keys [protocol type] :as agent-opts}]
   (let [type        (or (not-empty type) (not-empty protocol) "system")
         [opts set-opts!] (rum/use-state agent-opts)
         [testing? set-testing?!] (rum/use-state false)
         *test-input (rum/create-ref)
         disabled?   (or (= (:type opts) "system") (= (:type opts) "direct"))]
-    [:div.cp__settings-network-proxy-panel
+    [:div.cp__settings-network-proxy-cnt
      [:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
      [:div.p-2
       [:p [:label [:strong (t :type)]
@@ -478,6 +479,35 @@
                               (p/let [_ (ipc/ipc :setProxy opts)]
                                 (state/set-state! [:electron/user-cfgs :settings/agent] opts))))]]]))
 
+(rum/defc load-from-web-url-container
+  []
+  (let [[url set-url!] (rum/use-state "http://127.0.0.1:8080/")
+        [pending? set-pending?] (rum/use-state false)
+        handle-submit! (fn []
+                         (set-pending? true)
+                         (-> (plugin-handler/load-plugin-from-web-url! url)
+                             (p/then #(do (notification/show! "New plugin registered!" :success)
+                                          (shui/dialog-close!)))
+                             (p/catch #(notification/show! (str %) :error))
+                             (p/finally
+                               #(set-pending? false))))]
+
+    [:div.px-4.pt-4.pb-2.rounded-md.flex.flex-col.gap-2
+     [:div.flex.flex-col.gap-3
+      (shui/input {:placeholder "http://"
+                   :value url
+                   :on-change #(set-url! (-> (util/evalue %) (util/trim-safe)))
+                   :auto-focus true})
+      [:span.text-gray-10
+       (shui/tabler-icon "info-circle" {:size 13})
+       [:span "URLs support both GitHub repositories and local development servers.
+      (For examples: https://github.com/xyhp915/logseq-journals-calendar,
+      http://localhost:8080/<plugin-dir-root>)"]]]
+     [:div.flex.justify-end
+      (shui/button {:disabled (or pending? (string/blank? url))
+                    :on-click handle-submit!}
+                   (if pending? (ui/loading) "Install"))]]))
+
 (rum/defc auto-check-for-updates-control
   []
   (let [[enabled, set-enabled!] (rum/use-state (plugin-handler/get-enabled-auto-check-for-updates?))
@@ -614,17 +644,21 @@
 
                           [{:hr true}]
 
-                          (when (and (state/developer-mode?)
-                                     (util/electron?))
-                            [{:title [:span.flex.items-center.gap-1 (ui/icon "file-code") (t :plugin/open-preferences)]
-                              :options {:on-click
-                                        #(p/let [root (plugin-handler/get-ls-dotdir-root)]
-                                           (js/apis.openPath (str root "/preferences.json")))}}
-                             {:title [:span.flex.items-center.whitespace-nowrap.gap-1
-                                      (ui/icon "bug") (t :plugin/open-logseq-dir) [:code "~/.logseq"]]
-                              :options {:on-click
-                                        #(p/let [root (plugin-handler/get-ls-dotdir-root)]
-                                           (js/apis.openPath root))}}])
+                          (when (state/developer-mode?)
+                            (if (util/electron?)
+                              [{:title [:span.flex.items-center.gap-1 (ui/icon "file-code") (t :plugin/open-preferences)]
+                                :options {:on-click
+                                          #(p/let [root (plugin-handler/get-ls-dotdir-root)]
+                                             (js/apis.openPath (str root "/preferences.json")))}}
+                               {:title [:span.flex.items-center.whitespace-nowrap.gap-1
+                                        (ui/icon "bug") (t :plugin/open-logseq-dir) [:code "~/.logseq"]]
+                                :options {:on-click
+                                          #(p/let [root (plugin-handler/get-ls-dotdir-root)]
+                                             (js/apis.openPath root))}}]
+                              [{:title [:span.flex.items-center.whitespace-nowrap.gap-1
+                                        (ui/icon "plug") (t :plugin/load-from-web-url)]
+                                :options {:on-click
+                                          #(shui/dialog-open! load-from-web-url-container)}}]))
 
                           [{:title [:span.flex.items-center.gap-1 (ui/icon "alert-triangle") (t :plugin/report-security)]
                             :options {:on-click #(plugin-handler/open-report-modal!)}}]
@@ -664,7 +698,7 @@
   (let [^js inViewState (ui/useInView #js {:threshold 0})
         in-view?        (.-inView inViewState)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (load-more!))
      [in-view?])
@@ -1009,13 +1043,13 @@
          id      (str "slot__" rs)
          *el-ref (rum/use-ref nil)]
 
-     (rum/use-effect!
+     (hooks/use-effect!
       (fn []
         (let [timer (js/setTimeout #(callback {:type type :slot id :payload payload}) 50)]
           #(js/clearTimeout timer)))
       [id])
 
-     (rum/use-effect!
+     (hooks/use-effect!
       (fn []
         (let [el (rum/deref *el-ref)]
           #(when-let [uis (seq (.querySelectorAll el "[data-injected-ui]"))]
@@ -1039,7 +1073,7 @@
         uni    #(str prefix "injected-ui-item-" %)
         ^js pl (js/LSPluginCore.registeredPlugins.get (name pid))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js el (rum/deref *el)]
          (js/LSPlugin.pluginHelpers.setupInjectedUI.call
@@ -1121,7 +1155,7 @@
   (let [*wrap-el (rum/use-ref nil)
         [right-sidebar-resized] (rum-utils/use-atom ui-handler/*right-sidebar-resized-at)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js wrap-el (rum/deref *wrap-el)]
          (when-let [^js header-el (.closest wrap-el ".cp__header")]
@@ -1190,11 +1224,11 @@
         *cm (rum/use-ref nil)
         *el (rum/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(set-content1! content)
      [content])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (some-> (rum/deref *el)
                (.closest ".ui-fenced-code-wrap")
@@ -1208,7 +1242,7 @@
          (.setCursor cm (.lineCount cm) (count (.getLine cm (.lastLine cm))))))
      [editor-active?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [t (js/setTimeout
                 #(when-let [^js cm (some-> (rum/deref *el)
@@ -1245,13 +1279,13 @@
         market? (= active :marketplace)
         *el-ref (rum/create-ref)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (state/load-app-user-cfgs)
        #(clear-dirties-states!))
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(clear-dirties-states!)
      [market?])
 
@@ -1312,7 +1346,7 @@
                         (catch js/Error _
                           (set-uid (notification/show! content status false nil nil cb)))))))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (if check-pending?
          (notify!
@@ -1323,7 +1357,7 @@
          (when uid (notification/clear! uid))))
      [check-pending? sub-content])
 
-    (rum/use-effect!
+    (hooks/use-effect!
       ;; scheduler for auto updates
      (fn []
        (when online?
@@ -1333,10 +1367,12 @@
                           (not (number? last-updates))
                            ;; interval 12 hours
                           (> (- (js/Date.now) last-updates) (* 60 60 12 1000))))
-             (js/setTimeout
-              (fn []
-                (plugin-handler/auto-check-enabled-for-updates!)
-                (storage/set :lsp-last-auto-updates (js/Date.now))))))))
+             (let [update-timer (js/setTimeout
+                                 (fn []
+                                   (plugin-handler/auto-check-enabled-for-updates!)
+                                   (storage/set :lsp-last-auto-updates (js/Date.now)))
+                                 (if (util/electron?) 3000 (* 60 1000)))]
+               #(js/clearTimeout update-timer))))))
      [online?])
 
     [:<>]))
@@ -1391,7 +1427,7 @@
 
 (rum/defc custom-js-installer
   [{:keys [t current-repo db-restoring? nfs-granted?]}]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (when (and (not db-restoring?)
                 (or (not util/nfs?) nfs-granted?))

+ 2 - 1
src/main/frontend/components/plugins_settings.cljs

@@ -3,6 +3,7 @@
             [frontend.components.lazy-editor :as lazy-editor]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
+            [frontend.hooks :as hooks]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [goog.functions :refer [debounce]]
@@ -119,7 +120,7 @@
         [edit-mode, set-edit-mode!] (rum/use-state nil) ;; code
         update-setting! (fn [k v] (.set plugin-settings (name k) (bean/->js v)))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [on-change (fn [^js s]
                          (when-let [s (bean/->clj s)]

+ 9 - 4
src/main/frontend/components/property.cljs

@@ -18,6 +18,7 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
@@ -148,7 +149,7 @@
   (let [[properties set-properties!] (rum/use-state nil)
         [classes set-classes!] (rum/use-state nil)
         [excluded-properties set-excluded-properties!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/let [repo (state/get-current-repo)
                properties (db-async/<db-based-get-all-properties repo)
@@ -689,6 +690,10 @@
            (let [properties (->> (:logseq.property.class/properties block)
                                  (map (fn [e] [(:db/ident e)])))
                  opts' (assoc opts :class-schema? true)]
-             [:div
-              (properties-section block properties opts')
-              (rum/with-key (new-property block opts') (str id "-class-add-property"))]))]))))
+             [:<>
+              [:div.mt-2
+               [:div.text-sm.text-muted-foreground.mb-2 {:style {:margin-left 10}}
+                "Tag Properties:"]
+               [:div
+                (properties-section block properties opts')
+                (rum/with-key (new-property block opts') (str id "-class-add-property"))]]]))]))))

+ 35 - 23
src/main/frontend/components/property/config.cljs

@@ -15,6 +15,7 @@
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -83,7 +84,7 @@
   [property {:keys [multiple-choices? disabled? default-open? no-class? on-hide]
              :or {multiple-choices? true}}]
   (let [*ref (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when default-open?
          (some-> (rum/deref *ref)
@@ -160,7 +161,7 @@
         title (util/trim-safe (:title form-data))
         description (util/trim-safe (:description form-data))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
      [])
@@ -209,7 +210,7 @@
         [form-data, set-form-data!] (rum/use-state (rum/deref *form-data))
         *input-ref (rum/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when create?
          (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
@@ -467,14 +468,16 @@
         checked-choice (some (fn [choice] (when (true? (:logseq.property/choice-checkbox-state choice)) choice)) choices)
         unchecked-choice (some (fn [choice] (when (false? (:logseq.property/choice-checkbox-state choice)) choice)) choices)]
     [:div.flex.flex-col.gap-4.text-sm.p-2
-     [:div.text-muted-foreground "Checkbox state mapping"]
      [:div.flex.flex-col.gap-2
       [:div "Map unchecked to"]
       (select-cp
        (cond->
         {:on-value-change
          (fn [value]
-           (db-property-handler/set-block-property! value :logseq.property/choice-checkbox-state false))}
+           (p/do!
+            (db-property-handler/set-block-property! value :logseq.property/choice-checkbox-state false)
+            (when unchecked-choice
+              (db-property-handler/remove-block-property! (:db/id unchecked-choice) :logseq.property/choice-checkbox-state))))}
          unchecked-choice
          (assoc :default-value (:db/id unchecked-choice))))
 
@@ -483,7 +486,10 @@
        (cond->
         {:on-value-change
          (fn [value]
-           (db-property-handler/set-block-property! value :logseq.property/choice-checkbox-state true))}
+           (p/do!
+            (db-property-handler/set-block-property! value :logseq.property/choice-checkbox-state true)
+            (when checked-choice
+              (db-property-handler/remove-block-property! (:db/id checked-choice) :logseq.property/choice-checkbox-state))))}
          checked-choice
          (assoc :default-value (:db/id checked-choice))))]]))
 
@@ -635,23 +641,11 @@
      (when enable-closed-values?
        (let [values (:property/closed-values property)]
          (when (>= (count values) 2)
-           (let [checked? (contains?
-                           (set (map :db/id (:logseq.property/checkbox-display-properties owner-block)))
-                           (:db/id property))]
-             (dropdown-editor-menuitem
-              {:icon :checkbox :title "Show as checkbox"
-               :desc (when owner-block
-                       (shui/switch
-                        {:id "show as checkbox" :size "sm"
-                         :checked checked?
-                         :on-click util/stop-propagation
-                         :on-checked-change
-                         (fn [value]
-                           (if value
-                             (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
-                             (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))
-               :submenu-content (fn []
-                                  (checkbox-state-mapping values))})))))
+           (dropdown-editor-menuitem
+            {:icon :checkbox
+             :title "Checkbox state mapping"
+             :submenu-content (fn []
+                                (checkbox-state-mapping values))}))))
 
      (when (and (contains? db-property-type/cardinality-property-types property-type) (not disabled?))
        (let [many? (db-property/many? property)]
@@ -708,6 +702,24 @@
                        :on-select (fn []
                                     (shui/popup-hide-all!)
                                     (route-handler/redirect-to-page! (:block/uuid property)))}})])
+     (when enable-closed-values?
+       (let [values (:property/closed-values property)]
+         (when (>= (count values) 2)
+           (let [checked? (contains?
+                           (set (map :db/id (:logseq.property/checkbox-display-properties owner-block)))
+                           (:db/id property))]
+             (dropdown-editor-menuitem
+              {:icon :checkbox :title "Show as checkbox on node"
+               :desc (when owner-block
+                       (shui/switch
+                        {:id "show as checkbox" :size "sm"
+                         :checked checked?
+                         :on-click util/stop-propagation
+                         :on-checked-change
+                         (fn [value]
+                           (if value
+                             (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
+                             (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
 
      (when (and owner-block
                 ;; Any property should be removable from Tag Properties

+ 14 - 20
src/main/frontend/components/property/value.cljs

@@ -22,6 +22,7 @@
             [frontend.handler.property :as property-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
+            [frontend.hooks :as hooks]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.search :as search]
             [frontend.state :as state]
@@ -79,7 +80,7 @@
                         :logseq.property/icon))
                      (clear-overlay!))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when editing?
          (clear-overlay!)
@@ -251,7 +252,9 @@
                                                                                                (:db/id property))
                                                       (db-property-handler/remove-block-property! (:db/id block)
                                                                                                   :logseq.task/scheduled-on-property)))))]
-       [:div "Set as repeated task"]]]
+       (if (= :logseq.task/deadline (:db/ident property))
+         [:div "Set as repeated task"]
+         [:div "Repeat " (if (= :date (get-in property [:block/schema :type])) "date" "datetime")])]]
      [:div.flex.flex-row.gap-2
       [:div.flex.text-muted-foreground.mr-4
        "Every"]
@@ -278,7 +281,7 @@
                         (db/entity :logseq.task/status.done))]
        [:div.flex.flex-col.gap-2
         [:div.text-muted-foreground
-         "Reschedule when"]
+         "When"]
         (shui/select
          (cond->
           {:on-value-change (fn [v]
@@ -298,14 +301,6 @@
          (when done-choice
            (db-property/property-value-content done-choice))]])]))
 
-(defn- get-local-journal-date-time
-  [year month day]
-  (let [[op h m] (:offset (t/default-time-zone))
-        f (if (= op :-) t/plus t/minus)]
-    (-> (t/date-time year month day)
-        (f (t/hours h))
-        (f (t/minutes m)))))
-
 (rum/defcs calendar-inner < rum/reactive db-mixins/query
   (rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
   {:init (fn [state]
@@ -329,9 +324,8 @@
         value (cond
                 (map? value)
                 (when-let [day (:block/journal-day value)]
-                  (let [t (tc/to-date-time (date/journal-day->ts day))]
-                    (js/Date.
-                     (get-local-journal-date-time (t/year t) (t/month t) (t/day t)))))
+                  (let [t (date/journal-day->utc-ms day)]
+                    (js/Date. t)))
 
                 (number? value)
                 (js/Date. value)
@@ -379,7 +373,7 @@
 (rum/defc overdue
   [date content]
   (let [[current-time set-current-time!] (rum/use-state (t/now))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [timer (js/setInterval (fn [] (set-current-time! (t/now))) (* 1000 60 3))]
          #(js/clearInterval timer)))
@@ -450,7 +444,7 @@
                           (shui/popup-show! (.-target e) content-fn
                                             {:align "start" :auto-focus? true}))))
         repeated-task? (:logseq.task/repeated? block)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when editing?
          (js/setTimeout
@@ -477,7 +471,7 @@
           (ui/icon "repeat" {:size 14 :class "opacity-40"}))
         (cond
           (map? value)
-          (let [date (tc/to-date-time (date/journal-day->ts (:block/journal-day value)))
+          (let [date (tc/to-date-time (date/journal-day->utc-ms (:block/journal-day value)))
                 compare-value (some-> date
                                       (t/plus (t/days 1))
                                       (t/minus (t/seconds 1)))
@@ -791,7 +785,7 @@
         parent-property? (= (:db/ident property) :logseq.property/parent)]
     (when (and (not parent-property?) (seq non-root-classes))
       ;; effect runs once
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (p/let [result (p/all (map (fn [class] (db-async/<get-tag-objects repo (:db/id class))) non-root-classes))
                  result' (distinct (apply concat result))]
@@ -1030,7 +1024,7 @@
   [block property value value-f select-opts opts]
   (let [*el (rum/use-ref nil)]
     ;; Open popover initially when editing a property
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (:editing? opts)
          (.click (rum/deref *el))))
@@ -1171,7 +1165,7 @@
         items (cond->> (if (de/entity? v) #{v} v)
                 (= (:db/ident property) :block/tags)
                 (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when editing?
          (.click (rum/deref *el))))

+ 6 - 5
src/main/frontend/components/query.cljs

@@ -1,21 +1,22 @@
 (ns frontend.components.query
   (:require [clojure.string :as string]
-            [frontend.components.file-based.query-table :as query-table]
             [frontend.components.file-based.query :as file-query]
+            [frontend.components.file-based.query-table :as query-table]
             [frontend.components.query.result :as query-result]
             [frontend.components.query.view :as query-view]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.extensions.sci :as sci]
             [frontend.handler.editor :as editor-handler]
+            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
-            [rum.core :as rum]
-            [frontend.config :as config]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [rum.core :as rum]))
 
 (defn- built-in-custom-query?
   [title]
@@ -184,7 +185,7 @@
 (rum/defc trigger-custom-query
   [config q]
   (let [[result set-result!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (query-result/trigger-custom-query! config q (:*query-error config) set-result!))
      [q])

+ 4 - 3
src/main/frontend/components/query/builder.cljs

@@ -11,6 +11,7 @@
             [frontend.db.query-dsl :as query-dsl]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.query.builder :as query-builder]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -145,7 +146,7 @@
         properties (cond->> properties
                      (not @*private-property?)
                      (remove ldb/built-in?))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/let [properties (db-async/<get-all-properties {:remove-built-in-property? false
                                                          :remove-non-queryable-built-in-property? true})]
@@ -206,7 +207,7 @@
         property-type (when db-graph? (:property/type (db/entity repo @*property)))
         ref-property? (and db-graph? (contains? db-property-type/all-ref-property-types property-type))
         [values set-values!] (rum/use-state nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/let [result (if db-graph?
                         (db-async/<get-block-property-values repo @*property)
@@ -225,7 +226,7 @@
   [repo *tree opts loc]
   (let [[values set-values!] (rum/use-state nil)
         db-based? (config/db-based-graph? repo)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [result (db-model/get-all-readable-classes repo {:except-root-class? true})]
          (set-values! result)))

+ 3 - 2
src/main/frontend/components/right_sidebar.cljs

@@ -16,6 +16,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -305,7 +306,7 @@
                              width (str value "%")]
                          (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
                          (ui-handler/persist-right-sidebar-width! width))))]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
          (-> (js/interact el)
@@ -357,7 +358,7 @@
        #())
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
         ;; sidebar animation duration
        (js/setTimeout

+ 9 - 8
src/main/frontend/components/server.cljs

@@ -1,15 +1,16 @@
 (ns frontend.components.server
   (:require
    [clojure.string :as string]
-   [logseq.shui.ui :as shui]
-   [rum.core :as rum]
    [electron.ipc :as ipc]
-   [medley.core :as medley]
-   [promesa.core :as p]
+   [frontend.handler.notification :as notification]
+   [frontend.hooks :as hooks]
    [frontend.state :as state]
+   [frontend.ui :as ui]
    [frontend.util :as util]
-   [frontend.handler.notification :as notification]
-   [frontend.ui :as ui]))
+   [logseq.shui.ui :as shui]
+   [medley.core :as medley]
+   [promesa.core :as p]
+   [rum.core :as rum]))
 
 (rum/defcs panel-of-tokens
   < rum/reactive
@@ -124,7 +125,7 @@
 (rum/defc server-indicator
   [server-state]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (p/let [_ (p/delay 1000)
              _ (ipc/ipc :server/load-state)]
@@ -138,7 +139,7 @@
         running? (= :running status)
         href     (and running? (str "http://" (:host server-state) ":" (:port server-state)))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when error
         (notification/show! (str "[Server] " error) :error))
      [error])

+ 2 - 1
src/main/frontend/components/settings.cljs

@@ -21,6 +21,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.hooks :as hooks]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
@@ -1174,7 +1175,7 @@
   < rum/static
   [active]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [active (and (sequential? active) (name (first active)))
            ^js ds (.-dataset js/document.body)]

+ 1 - 1
src/main/frontend/components/settings.css

@@ -203,7 +203,7 @@
     }
   }
 
-  &-network-proxy-panel {
+  &-network-proxy-cnt {
     margin: -15px 0;
 
     label {

+ 6 - 5
src/main/frontend/components/shortcut.cljs

@@ -3,6 +3,7 @@
             [clojure.string :as string]
             [frontend.context.i18n :refer [t]]
             [frontend.handler.notification :as notification]
+            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.data-helper :as dh]
@@ -46,7 +47,7 @@
 
   (let [keypressed? (not= "" keystroke)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [key-handler (KeyHandler. js/document)]
           ;; setup
@@ -212,7 +213,7 @@
         [current-binding set-current-binding!] (rum/use-state (or user-binding binding))
         [key-conflicts set-key-conflicts!] (rum/use-state nil)
 
-        handler-id (rum/use-memo #(dh/get-group k))
+        handler-id (hooks/use-memo #(dh/get-group k) [])
         dirty? (not= (or user-binding binding) current-binding)
         keypressed? (not= "" keystroke)
         save-keystroke-fn!
@@ -235,7 +236,7 @@
                 (set-key-conflicts! conflicts-map)))))]
 
     ;; TODO: back interaction for the shui dialog
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [mid (shui-dialog/get-first-modal-id)
              mid' (shui-dialog/get-last-modal-id)
@@ -248,7 +249,7 @@
                      (.click)) 200))))
      [modal-life])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js el (rum/deref *ref-el)
              key-handler (KeyHandler. el)
@@ -384,7 +385,7 @@
                               (set-folded-categories! #{})
                               (set-folded-categories! all-categories))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (js/setTimeout #(set-ready! true) 100))
      [])

+ 15 - 14
src/main/frontend/components/theme.cljs

@@ -8,6 +8,7 @@
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.hooks :as hooks]
             [frontend.rum :refer [use-mounted]]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -19,7 +20,7 @@
 (rum/defc scrollbar-measure
   []
   (let [*el (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [el (rum/deref *el)]
          (let [w (- (.-offsetWidth el) (.-clientWidth el))
@@ -40,7 +41,7 @@
   (let [mounted-fn (use-mounted)
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(let [^js doc js/document.documentElement
             ^js cls (.-classList doc)
             ^js cls-body (.-classList js/document.body)]
@@ -54,36 +55,36 @@
      [theme])
 
     ;; theme color
-    (rum/use-effect!
+    (hooks/use-effect!
      #(some-> js/document.documentElement
               (.setAttribute "data-color"
                              (or accent-color "logseq")))
      [accent-color])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(some-> js/document.documentElement
               (.setAttribute "data-font" (or editor-font "default")))
      [editor-font])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(let [doc js/document.documentElement]
         (.setAttribute doc "lang" preferred-language)))
 
-    (rum/use-effect!
+    (hooks/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
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when (and restored-sidebar?
                  (mounted-fn))
         (plugin-handler/hook-plugin-app :sidebar-visible-changed {:visible sidebar-open?})
         (ui-handler/persist-right-sidebar-state!))
      [sidebar-open? restored-sidebar? sidebar-blocks-len])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when config/lsp-enabled?
         (plugin-handler/load-plugin-preferences)
         (comp
@@ -91,14 +92,14 @@
          (plugin-config-handler/setup-install-listener!)))
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (ui-handler/reset-custom-css!)
        (pdf/reset-current-pdf!)
        (plugin-handler/hook-plugin-app :current-graph-changed {}))
      [current-repo])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(let [db-restored? (false? db-restoring?)]
         (if db-restoring?
           (util/set-title! (t :loading))
@@ -106,7 +107,7 @@
             (route-handler/update-page-title! route))))
      [nfs-granted? db-restoring? route])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-not db-restoring?
          (let [repos (state/get-repos)]
@@ -121,12 +122,12 @@
                (set-restored-sidebar? true))))))
      [db-restoring?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when system-theme?
         (ui/setup-system-theme-effect!))
      [system-theme?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (if settings-open?
          (shui/dialog-open!
@@ -138,7 +139,7 @@
          (shui/dialog-close! :app-settings)))
      [settings-open?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      #(storage/set :file-sync/onboarding-state onboarding-state)
      [onboarding-state])
 

+ 59 - 58
src/main/frontend/components/user/login.cljs

@@ -1,16 +1,17 @@
 (ns frontend.components.user.login
-  (:require [clojure.string :as string]
-            [logseq.shui.ui :as shui]
-            [rum.core :as rum]
+  (:require [cljs-bean.core :as bean]
+            [clojure.string :as string]
             [dommy.core :refer-macros [sel]]
-            [frontend.rum :refer [adapt-class]]
-            [frontend.modules.shortcut.core :as shortcut]
-            [frontend.handler.user :as user]
-            [frontend.handler.route :as route-handler]
-            [cljs-bean.core :as bean]
+            [frontend.config :as config]
             [frontend.handler.notification :as notification]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.user :as user]
+            [frontend.hooks :as hooks]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.rum :refer [adapt-class]]
             [frontend.state :as state]
-            [frontend.config :as config]))
+            [logseq.shui.ui :as shui]
+            [rum.core :as rum]))
 
 (declare setupAuthConfigure! LSAuthenticator)
 
@@ -29,26 +30,26 @@
 
   (.setLanguage js/LSAmplify.I18n (or (:preferred-language @state/state) "en"))
   (setupAuthConfigure!
-    #js {:region              config/REGION,
-         :userPoolId          config/USER-POOL-ID,
-         :userPoolWebClientId config/COGNITO-CLIENT-ID,
-         :identityPoolId      config/IDENTITY-POOL-ID,
-         :oauthDomain         config/OAUTH-DOMAIN}))
+   #js {:region              config/REGION,
+        :userPoolId          config/USER-POOL-ID,
+        :userPoolWebClientId config/COGNITO-CLIENT-ID,
+        :identityPoolId      config/IDENTITY-POOL-ID,
+        :oauthDomain         config/OAUTH-DOMAIN}))
 
 (rum/defc user-pane
   [_sign-out! user]
   (let [session  (:signInUserSession user)
         username (:username user)]
 
-    (rum/use-effect!
-      (fn []
-        (when session
-          (user/login-callback session)
-          (notification/show! (str "Hi, " username " :)") :success)
-          (shui/dialog-close!)
-          (when (= :user-login (state/get-current-route))
-            (route-handler/redirect! {:to :home}))))
-      [])
+    (hooks/use-effect!
+     (fn []
+       (when session
+         (user/login-callback session)
+         (notification/show! (str "Hi, " username " :)") :success)
+         (shui/dialog-close!)
+         (when (= :user-login (state/get-current-route))
+           (route-handler/redirect! {:to :home}))))
+     [])
 
     nil))
 
@@ -58,41 +59,41 @@
         [tab, set-tab!] (rum/use-state :login)
         *ref-el (rum/use-ref nil)]
 
-    (rum/use-effect!
-      (fn [] (setup-configure!)
-        (set-ready? true)
-        (js/setTimeout
-          (fn []
-            (when-let [^js el (some-> (rum/deref *ref-el) (.querySelector ".amplify-tabs"))]
-              (let [btn1 (.querySelector el "button")]
-                (.addEventListener el "pointerdown"
-                  (fn [^js e]
-                    (if (= (.-target e) btn1)
-                      (set-tab! :login)
-                      (set-tab! :create-account)))))))))
-      [])
+    (hooks/use-effect!
+     (fn [] (setup-configure!)
+       (set-ready? true)
+       (js/setTimeout
+        (fn []
+          (when-let [^js el (some-> (rum/deref *ref-el) (.querySelector ".amplify-tabs"))]
+            (let [btn1 (.querySelector el "button")]
+              (.addEventListener el "pointerdown"
+                                 (fn [^js e]
+                                   (if (= (.-target e) btn1)
+                                     (set-tab! :login)
+                                     (set-tab! :create-account)))))))))
+     [])
 
-    (rum/use-effect!
-      (fn []
-        (when-let [^js el (rum/deref *ref-el)]
-          (js/setTimeout
-            #(some-> (.querySelector el (str "input[name=" (if (= tab :login) "username" "email") "]"))
-               (.focus)) 100)))
-      [tab])
+    (hooks/use-effect!
+     (fn []
+       (when-let [^js el (rum/deref *ref-el)]
+         (js/setTimeout
+          #(some-> (.querySelector el (str "input[name=" (if (= tab :login) "username" "email") "]"))
+                   (.focus)) 100)))
+     [tab])
 
     [:div.cp__user-login
      {:ref *ref-el}
      (when ready?
        (LSAuthenticator
-         {:termsLink "https://blog.logseq.com/terms/"}
-         (fn [^js op]
-           (let [sign-out!'      (.-signOut op)
-                 ^js user-proxy (.-user op)
-                 ^js user       (try (js/JSON.parse (js/JSON.stringify user-proxy))
-                                     (catch js/Error e
-                                       (js/console.error "Error: Amplify user payload:" e)))
-                 user'          (bean/->clj user)]
-             (user-pane sign-out!' user')))))]))
+        {:termsLink "https://blog.logseq.com/terms/"}
+        (fn [^js op]
+          (let [sign-out!'      (.-signOut op)
+                ^js user-proxy (.-user op)
+                ^js user       (try (js/JSON.parse (js/JSON.stringify user-proxy))
+                                    (catch js/Error e
+                                      (js/console.error "Error: Amplify user payload:" e)))
+                user'          (bean/->clj user)]
+            (user-pane sign-out!' user')))))]))
 
 (rum/defcs modal-inner <
   shortcut/disable-all-shortcuts
@@ -106,9 +107,9 @@
 (defn open-login-modal!
   []
   (shui/dialog-open!
-    (fn [_close] (modal-inner))
-    {:label "user-login"
-     :content-props {:onPointerDownOutside #(let [inputs (sel "form[data-amplify-form] input:not([type=checkbox])")
-                                                  inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
-                                              (when (seq inputs)
-                                                (.preventDefault %)))}}))
+   (fn [_close] (modal-inner))
+   {:label "user-login"
+    :content-props {:onPointerDownOutside #(let [inputs (sel "form[data-amplify-form] input:not([type=checkbox])")
+                                                 inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
+                                             (when (seq inputs)
+                                               (.preventDefault %)))}}))

+ 10 - 8
src/main/frontend/components/views.cljs

@@ -17,6 +17,7 @@
             [frontend.db-mixins :as db-mixins]
             [frontend.handler.property :as property-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -56,7 +57,8 @@
 (rum/defc header-index < rum/static
   []
   [:label.h-8.w-6.flex.items-center.justify-center
-   {:html-for "header-index"}
+   {:html-for "header-index"
+    :title "Row number"}
    "ID"])
 
 (rum/defc row-checkbox < rum/static
@@ -334,14 +336,14 @@
         add-resizing-class #(dom/add-class! js/document.documentElement "is-resizing-buf")
         remove-resizing-class #(dom/remove-class! js/document.documentElement "is-resizing-buf")]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (number? dx)
          (some-> (rum/deref *el)
                  (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)")))))
      [dx])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref *el))]
          (let [*field-rect (atom nil)
@@ -398,7 +400,7 @@
                (.on "mousedown" util/stop-propagation)))))
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when (number? width)
          (on-sized! width)))
@@ -455,7 +457,7 @@
   [table row column render cell-opts idx first-col-rendered? set-first-col-rendered!]
   (let [primary-key? (or (= idx 1) (= (:id column) :block/title))]
     (when primary-key?
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (let [timeout (js/setTimeout #(set-first-col-rendered! true) 0)]
            #(js/clearTimeout timeout)))
@@ -1084,7 +1086,7 @@
         [ready? set-ready?] (rum/use-state false)
         *rows-wrap (rum/use-ref nil)]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn [] (set-ready? true))
      [])
 
@@ -1157,7 +1159,7 @@
   [option {:keys [data columns state data-fns]} input input-filters set-input-filters! *scroller-ref gallery?]
   (let [{:keys [filters sorting]} state
         {:keys [set-row-filter! set-data!]} data-fns]
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [new-input-filters [input filters]]
          (when-not (= input-filters new-input-filters)
@@ -1168,7 +1170,7 @@
                 (row-matched? row input filters)))))))
      [input filters])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        ;; Entities might be outdated
        (let [;; TODO: should avoid this for better performance, 300ms for 40k pages

+ 1 - 2
src/main/frontend/core.cljs

@@ -3,7 +3,6 @@
   {:dev/always true}
   (:require [frontend.background-tasks]
             [frontend.common-keywords]
-            [frontend.common.schema-register :as sr]
             [frontend.components.plugins :as plugins]
             [frontend.config :as config]
             [frontend.fs.sync :as sync]
@@ -15,6 +14,7 @@
             [frontend.routes :as routes]
             [frontend.spec]
             [logseq.api]
+            [logseq.db.frontend.kv-entity]
             [malli.dev.cljs :as md]
             [reitit.frontend :as rf]
             [reitit.frontend.easy :as rfe]
@@ -50,7 +50,6 @@
 (defn ^:export start []
   (when config/dev?
     (md/start!))
-  (frontend.common.schema-register/init)
   (when-let [node (.getElementById js/document "root")]
     (set-router!)
     (rum/mount (page/current-page) node)

+ 1 - 6
src/main/frontend/date.cljs

@@ -136,12 +136,7 @@
    journal-title
    (date-time-util/safe-journal-title-formatters (state/get-date-formatter))))
 
-(defn journal-day->ts
-  "journal-day format yyyyMMdd"
-  [day]
-  (when day
-    (-> (tf/parse (tf/formatter "yyyyMMdd") (str day))
-        (tc/to-long))))
+(def journal-day->utc-ms date-time-util/journal-day->ms)
 
 (defn journal-title->long
   [journal-title]

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

@@ -234,7 +234,7 @@
           future-day (some->> future-date
                               (tf/unparse date-format)
                               (parse-long))
-          start-time (date/journal-day->ts date)
+          start-time (date/journal-day->utc-ms date)
           future-time (tc/to-long future-date)]
       (when-let [repo (and future-day (state/get-current-repo))]
         (p/let [result

+ 164 - 162
src/main/frontend/extensions/handbooks/core.cljs

@@ -1,34 +1,35 @@
 (ns frontend.extensions.handbooks.core
-  (:require [clojure.string :as string]
-            [rum.core :as rum]
+  (:require [camel-snake-kebab.core :as csk]
+            [cljs-bean.core :as bean]
             [cljs.core.async :as async :refer [<! >!]]
-            [frontend.ui :as ui]
-            [frontend.state :as state]
-            [frontend.search :as search]
+            [clojure.edn :as edn]
+            [clojure.string :as string]
             [frontend.config :as config]
-            [frontend.handler.notification :as notification]
+            [frontend.context.i18n :refer [t]]
             [frontend.extensions.lightbox :as lightbox]
+            [frontend.extensions.video.youtube :as youtube]
+            [frontend.handler.notification :as notification]
+            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.rum :as r]
-            [cljs-bean.core :as bean]
-            [promesa.core :as p]
-            [camel-snake-kebab.core :as csk]
-            [medley.core :as medley]
-            [frontend.util :as util]
+            [frontend.search :as search]
+            [frontend.state :as state]
             [frontend.storage :as storage]
-            [frontend.extensions.video.youtube :as youtube]
-            [frontend.context.i18n :refer [t]]
-            [clojure.edn :as edn]))
+            [frontend.ui :as ui]
+            [frontend.util :as util]
+            [medley.core :as medley]
+            [promesa.core :as p]
+            [rum.core :as rum]))
 
 (defonce *config (atom {}))
 
 (defn get-handbooks-endpoint
   [resource]
   (str
-    (if (storage/get :handbooks-dev-watch?)
-      "http://localhost:1337"
-      "https://handbooks.pages.dev")
-    resource))
+   (if (storage/get :handbooks-dev-watch?)
+     "http://localhost:1337"
+     "https://handbooks.pages.dev")
+   resource))
 
 (defn resolve-asset-url
   [path]
@@ -42,10 +43,10 @@
   (if-let [matches (and (not (string/blank? content))
                         (re-seq #"src=\"([^\"]+)\"" content))]
     (reduce
-      (fn [content matched]
-        (if-let [matched (second matched)]
-          (string/replace content matched (resolve-asset-url matched)) content))
-      content matches)
+     (fn [content matched]
+       (if-let [matched (second matched)]
+         (string/replace content matched (resolve-asset-url matched)) content))
+     content matches)
     content))
 
 (defn parse-key-from-href
@@ -68,9 +69,9 @@
 (defn bind-parent-key
   [{:keys [key] :as node}]
   (cond-> node
-          (and (string? key)
-               (string/includes? key "/"))
-          (assoc :parent (parse-parent-key key))))
+    (and (string? key)
+         (string/includes? key "/"))
+    (assoc :parent (parse-parent-key key))))
 
 (defn load-glide-assets!
   []
@@ -83,8 +84,8 @@
   [{:keys [key title description cover] :as _topic} nav-fn! opts]
   [:button.w-full.topic-card.flex.text-left
    (merge
-     {:key      key
-      :on-click nav-fn!} opts)
+    {:key      key
+     :on-click nav-fn!} opts)
    (when cover
      [:div.l.flex.items-center
       [:img {:src (resolve-asset-url cover)}]])
@@ -122,15 +123,15 @@
 (rum/defc chapter-select
   [topic children on-select]
   (let [[open?, set-open?] (rum/use-state false)]
-    (rum/use-effect!
-      (fn []
-        (when-let [^js el (js/document.querySelector "[data-identity=logseq-handbooks]")]
-          (let [h #(when-not (some->> (.-target %)
-                                      (.contains (js/document.querySelector ".chapters-select")))
-                     (set-open? false))]
-            (.addEventListener el "click" h)
-            #(.removeEventListener el "click" h))))
-      [])
+    (hooks/use-effect!
+     (fn []
+       (when-let [^js el (js/document.querySelector "[data-identity=logseq-handbooks]")]
+         (let [h #(when-not (some->> (.-target %)
+                                     (.contains (js/document.querySelector ".chapters-select")))
+                    (set-open? false))]
+           (.addEventListener el "click" h)
+           #(.removeEventListener el "click" h))))
+     [])
 
     [:div.chapters-select.w-full
      [:a.select-trigger
@@ -157,21 +158,21 @@
         *id-ref (rum/use-ref (str "glide--" (js/Date.now)))]
 
     ;; load deps assets
-    (rum/use-effect!
-      (fn []
-        (set-deps-pending? true)
-        (-> (load-glide-assets!)
-            (p/then (fn [] (js/setTimeout
-                             #(when (js/document.getElementById (rum/deref *id-ref))
-                                (doto (js/window.Glide. (str "#" (rum/deref *id-ref))) (.mount))) 50)))
-            (p/finally #(set-deps-pending? false))))
-      [])
-
-    (rum/use-effect!
-      (fn []
-        (js/setTimeout #(some-> (js/document.querySelector ".cp__handbooks-content")
-                                (.scrollTo 0 0))))
-      [pane-state])
+    (hooks/use-effect!
+     (fn []
+       (set-deps-pending? true)
+       (-> (load-glide-assets!)
+           (p/then (fn [] (js/setTimeout
+                           #(when (js/document.getElementById (rum/deref *id-ref))
+                              (doto (js/window.Glide. (str "#" (rum/deref *id-ref))) (.mount))) 50)))
+           (p/finally #(set-deps-pending? false))))
+     [])
+
+    (hooks/use-effect!
+     (fn []
+       (js/setTimeout #(some-> (js/document.querySelector ".cp__handbooks-content")
+                               (.scrollTo 0 0))))
+     [pane-state])
 
     (when-let [topic-key (:key (second pane-state))]
       (when-let [topic (get handbook-nodes topic-key)]
@@ -197,15 +198,15 @@
              (when show-chapters?
                [:div.chapters-wrap.py-2
                 (chapter-select
-                  topic chapters
-                  (fn [k]
-                    (when-let [chapter (get handbook-nodes k)]
-                      (nav! [:topic-detail chapter (:title parent)] pane-state))))])
+                 topic chapters
+                 (fn [k]
+                   (when-let [chapter (get handbook-nodes k)]
+                     (nav! [:topic-detail chapter (:title parent)] pane-state))))])
 
              ;; demos gallery
              (when-let [demos (:demos topic)]
                (let [demos (cond-> demos
-                                   (string? demos) (list))]
+                             (string? demos) (list))]
                  (if (> (count demos) 1)
                    [:div.flex.demos.glide
                     {:id (rum/deref *id-ref)}
@@ -218,10 +219,10 @@
 
                     [:div.glide__bullets {:data-glide-el "controls[nav]"}
                      (map-indexed
-                       (fn [idx _]
-                         [:button.glide__bullet {:data-glide-dir (str "=" idx)}
-                          (inc idx)])
-                       demos)]]
+                      (fn [idx _]
+                        [:button.glide__bullet {:data-glide-dir (str "=" idx)}
+                         (inc idx)])
+                      demos)]]
 
                    [:div.flex.demos.pt-1
                     (media-render (resolve-asset-url (first demos)))])))
@@ -321,39 +322,39 @@
         [selected, set-selected!] (rum/use-state 0)
         select-fn! #(when-let [ldx (and (seq results) (dec (count results)))]
                       (set-selected!
-                        (case %
-                          :up (if (zero? selected) ldx (max (dec selected) 0))
-                          :down (if (= selected ldx) 0 (min (inc selected) ldx))
-                          :dune)))
+                       (case %
+                         :up (if (zero? selected) ldx (max (dec selected) 0))
+                         :down (if (= selected ldx) 0 (min (inc selected) ldx))
+                         :dune)))
 
         q (util/trim-safe q)
         active? (not (string/blank? (util/trim-safe q)))
         reset-q! #(->> "" (set! (.-value (rum/deref *input-ref))) (set-q!))
         focus-q! #(some-> (rum/deref *input-ref) (.focus))]
 
-    (rum/use-effect!
-      #(focus-q!)
-      [pane-state])
+    (hooks/use-effect!
+     #(focus-q!)
+     [pane-state])
 
-    (rum/use-effect!
-      (fn []
-        (let [pane-nodes (:children (second pane-state))
-              pane-nodes (and (seq pane-nodes)
-                              (mapcat #(conj (:children %) %) pane-nodes))]
+    (hooks/use-effect!
+     (fn []
+       (let [pane-nodes (:children (second pane-state))
+             pane-nodes (and (seq pane-nodes)
+                             (mapcat #(conj (:children %) %) pane-nodes))]
 
-          (set-search-state!
-            (merge search-state {:active? active?}))
+         (set-search-state!
+          (merge search-state {:active? active?}))
 
-          (if (and (seq handbooks-nodes) active?)
-            (-> (or pane-nodes
+         (if (and (seq handbooks-nodes) active?)
+           (-> (or pane-nodes
                     ;; global
-                    (vals (dissoc handbooks-nodes "__root")))
-                (search/fuzzy-search q :limit 30 :extract-fn :title)
-                (set-results!))
-            (set-results! nil))
+                   (vals (dissoc handbooks-nodes "__root")))
+               (search/fuzzy-search q :limit 30 :extract-fn :title)
+               (set-results!))
+           (set-results! nil))
 
-          (set-selected! 0)))
-      [q])
+         (set-selected! 0)))
+     [q])
 
     [:div.search
      [:div.input-wrap.relative
@@ -414,8 +415,8 @@
   (let [{:keys [href]} opts]
     [:div.link-card
      (cond-> opts
-             (string? href)
-             (assoc :on-click #(util/open-url href)))
+       (string? href)
+       (assoc :on-click #(util/open-url href)))
      child]))
 
 ;(rum/defc related-topics
@@ -429,7 +430,6 @@
    :topic-detail [pane-topic-detail]
    :settings     [pane-settings]})
 
-
 (defonce discord-endpoint "https://plugins.logseq.io/ds")
 
 (rum/defc footer-link-cards
@@ -437,45 +437,45 @@
   (let [[config _] (r/use-atom *config)
         discord-count (:discord-online config)]
 
-    (rum/use-effect!
-      (fn []
-        (when (or (nil? discord-count)
-                  (> (- (js/Date.now) (:discord-online-created config)) (* 10 60 1000)))
-          (-> (js/window.fetch discord-endpoint)
-              (p/then #(.json %))
-              (p/then #(when-let [count (.-approximate_presence_count ^js %)]
-                         (swap! *config assoc
-                                :discord-online (.toLocaleString count)
-                                :discord-online-created (js/Date.now)))))))
-      [discord-count])
+    (hooks/use-effect!
+     (fn []
+       (when (or (nil? discord-count)
+                 (> (- (js/Date.now) (:discord-online-created config)) (* 10 60 1000)))
+         (-> (js/window.fetch discord-endpoint)
+             (p/then #(.json %))
+             (p/then #(when-let [count (.-approximate_presence_count ^js %)]
+                        (swap! *config assoc
+                               :discord-online (.toLocaleString count)
+                               :discord-online-created (js/Date.now)))))))
+     [discord-count])
 
     [:<>
      ;; more links
      [:div.flex.space-x-3
       {:style {:padding-top "4px"}}
       (link-card
-        {:class "flex-1" :href "https://discord.gg/KpN4eHY"}
-        [:div.inner.flex.space-x-1.flex-col
-         (ui/icon "brand-discord" {:class "opacity-30" :size 26})
-         [:h1.font-medium.py-1 "Chat on Discord"]
-         [:h2.text-xs.leading-4.opacity-40 "Ask quick questions, meet fellow users, and learn new workflows."]
-         [:small.flex.items-center.pt-1.5
-          [:i.block.rounded-full.bg-green-500 {:style {:width "8px" :height "8px"}}]
-          [:span.pl-2.opacity-90
-           [:strong.opacity-60 (or discord-count "?")]
-           [:span.opacity-70.font-light " users online"]]]])
+       {:class "flex-1" :href "https://discord.gg/KpN4eHY"}
+       [:div.inner.flex.space-x-1.flex-col
+        (ui/icon "brand-discord" {:class "opacity-30" :size 26})
+        [:h1.font-medium.py-1 "Chat on Discord"]
+        [:h2.text-xs.leading-4.opacity-40 "Ask quick questions, meet fellow users, and learn new workflows."]
+        [:small.flex.items-center.pt-1.5
+         [:i.block.rounded-full.bg-green-500 {:style {:width "8px" :height "8px"}}]
+         [:span.pl-2.opacity-90
+          [:strong.opacity-60 (or discord-count "?")]
+          [:span.opacity-70.font-light " users online"]]]])
 
       (link-card
-        {:class "flex-1" :href "https://discuss.logseq.com"}
-        [:div.inner.flex.space-x-1.flex-col
-         (ui/icon "message-dots" {:class "opacity-30" :size 26})
-         [:h1.font-medium.py-1 "Visit the forum"]
-         [:h2.text-xs.leading-4.opacity-40 "Give feedback, request features, and have in-depth conversations."]
-         [:small.flex.items-center.pt-1.5
-          [:i.flex.items-center.opacity-50 (ui/icon "bolt" {:size 14})]
-          [:span.pl-1.opacity-90
-           [:strong.opacity-60 "800+"]
-           [:span.opacity-70.font-light " monthly posts"]]]])]]))
+       {:class "flex-1" :href "https://discuss.logseq.com"}
+       [:div.inner.flex.space-x-1.flex-col
+        (ui/icon "message-dots" {:class "opacity-30" :size 26})
+        [:h1.font-medium.py-1 "Visit the forum"]
+        [:h2.text-xs.leading-4.opacity-40 "Give feedback, request features, and have in-depth conversations."]
+        [:small.flex.items-center.pt-1.5
+         [:i.flex.items-center.opacity-50 (ui/icon "bolt" {:size 14})]
+         [:span.pl-1.opacity-90
+          [:strong.opacity-60 "800+"]
+          [:span.opacity-70.font-light " monthly posts"]]]])]]))
 
 (rum/defc ^:large-vars/data-var content
   []
@@ -529,55 +529,57 @@
                                                    (apply = (map parse-parent-key [prev-key next-key]))))]
                          (when-not in-chapters?
                            (set-history-state!
-                             (conj (sequence history-state) prev-state))))
+                            (conj (sequence history-state) prev-state))))
                        (set-active-pane-state! next-state))
 
         [scrolled?, set-scrolled!] (rum/use-state false)
-        on-scroll (rum/use-memo #(util/debounce 100 (fn [^js e] (set-scrolled! (not (< (.. e -target -scrollTop) 10))))) [])]
+        on-scroll (hooks/use-memo
+                   #(util/debounce 100 (fn [^js e] (set-scrolled! (not (< (.. e -target -scrollTop) 10)))))
+                   [])]
 
     ;; load handbooks
-    (rum/use-effect!
-      #(load-handbooks!)
-      [])
+    (hooks/use-effect!
+     #(load-handbooks!)
+     [])
 
     ;; navigation sentry
-    (rum/use-effect!
-      (fn []
-        (when (seq handbooks-nodes)
-          (let [c (:handbook/route-chan @state/state)]
-            (async/go-loop []
-                           (let [v (<! c)]
-                             (when (not= v :return)
-                               (when-let [to (get handbooks-nodes v)]
-                                 (nav-to-pane! [:topic-detail to (t :handbook/title)] [:dashboard]))
-                               (recur))))
-            #(async/go (>! c :return)))))
-      [handbooks-nodes])
-
-    (rum/use-effect!
-      (fn []
-        (let [*cnt-len (atom 0)
-              check! (fn []
-                       (-> (p/let [^js res (js/fetch (get-handbooks-endpoint "/handbooks.edn") #js{:method "HEAD"})]
-                             (when-let [cl (.get (.-headers res) "content-length")]
-                               (when (not= @*cnt-len cl)
-                                 (println "[Handbooks] dev reload!")
-                                 (load-handbooks!))
-                               (reset! *cnt-len cl)))
-                           (p/catch #(println "[Handbooks] dev check Error:" %))))
-              timer0 (if dev-watch?
-                       (js/setInterval check! 2000) 0)]
-          #(js/clearInterval timer0)))
-      [dev-watch?])
-
-    (rum/use-effect!
-      (fn []
-        (when handbooks-data
-          (let [nodes (->> (tree-seq map? :children handbooks-data)
-                           (reduce #(assoc %1 (or (:key %2) "__root") (bind-parent-key %2)) {}))]
-            (set-handbooks-nodes! nodes)
-            (set! (.-handbook-nodes js/window) (bean/->js nodes)))))
-      [handbooks-data])
+    (hooks/use-effect!
+     (fn []
+       (when (seq handbooks-nodes)
+         (let [c (:handbook/route-chan @state/state)]
+           (async/go-loop []
+             (let [v (<! c)]
+               (when (not= v :return)
+                 (when-let [to (get handbooks-nodes v)]
+                   (nav-to-pane! [:topic-detail to (t :handbook/title)] [:dashboard]))
+                 (recur))))
+           #(async/go (>! c :return)))))
+     [handbooks-nodes])
+
+    (hooks/use-effect!
+     (fn []
+       (let [*cnt-len (atom 0)
+             check! (fn []
+                      (-> (p/let [^js res (js/fetch (get-handbooks-endpoint "/handbooks.edn") #js{:method "HEAD"})]
+                            (when-let [cl (.get (.-headers res) "content-length")]
+                              (when (not= @*cnt-len cl)
+                                (println "[Handbooks] dev reload!")
+                                (load-handbooks!))
+                              (reset! *cnt-len cl)))
+                          (p/catch #(println "[Handbooks] dev check Error:" %))))
+             timer0 (if dev-watch?
+                      (js/setInterval check! 2000) 0)]
+         #(js/clearInterval timer0)))
+     [dev-watch?])
+
+    (hooks/use-effect!
+     (fn []
+       (when handbooks-data
+         (let [nodes (->> (tree-seq map? :children handbooks-data)
+                          (reduce #(assoc %1 (or (:key %2) "__root") (bind-parent-key %2)) {}))]
+           (set-handbooks-nodes! nodes)
+           (set! (.-handbook-nodes js/window) (bean/->js nodes)))))
+     [handbooks-data])
 
     [:div.cp__handbooks-content
      {:class     (util/classnames [{:search-active (:active? search-state)
@@ -591,8 +593,8 @@
           [:button.active:opacity-80.flex.items-center.cursor-pointer
            {:on-click (fn [] (let [prev (first history-state)
                                    prev (cond-> prev
-                                                (nil? (seq prev))
-                                                [:dashboard])]
+                                          (nil? (seq prev))
+                                          [:dashboard])]
                                (set-active-pane-state! prev)
                                (set-history-state! (rest history-state))))}
            [:span.pr-2.flex.items-center (ui/icon "chevron-left")]
@@ -609,8 +611,8 @@
                           (let [s (str "logseq://handbook/" (:key (second active-pane-state)))]
                             (util/copy-to-clipboard! s)
                             (notification/show!
-                              [:div [:strong.block "Handbook link copied!"]
-                               [:label.opacity-50 s]] :success)))}
+                             [:div [:strong.block "Handbook link copied!"]
+                              [:label.opacity-50 s]] :success)))}
            (ui/icon "copy")])
         (when (state/developer-mode?)
           [:a.flex.items-center {:aria-label (t :handbook/settings)

+ 53 - 49
src/main/frontend/extensions/pdf/core.cljs

@@ -1,29 +1,30 @@
 (ns frontend.extensions.pdf.core
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
-            [frontend.components.svg :as svg]
+            [datascript.impl.entity :as de]
+            [frontend.commands :as commands]
             [frontend.components.block :as block]
+            [frontend.components.svg :as svg]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
+            [frontend.db.async :as db-async]
             [frontend.extensions.pdf.assets :as pdf-assets]
-            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.extensions.pdf.toolbar :refer [pdf-toolbar *area-dashed? *area-mode? *highlight-mode? *highlights-ctx*]]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.extensions.pdf.windows :as pdf-windows]
             [frontend.handler.notification :as notification]
-            [frontend.config :as config]
+            [frontend.handler.property :as property-handler]
+            [frontend.hooks :as hooks]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.commands :as commands]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
+            [frontend.ui :as ui]
             [frontend.util :as util]
+            [goog.functions :refer [debounce]]
             [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.ui :as ui]
-            [frontend.db.async :as db-async]
-            [goog.functions :refer [debounce]]
-            [frontend.handler.property :as property-handler]
-            [datascript.impl.entity :as de]))
+            [rum.core :as rum]))
 
 (declare pdf-container system-embed-playground)
 
@@ -57,7 +58,7 @@
 
 (rum/defc pdf-page-finder < rum/static
   [^js viewer]
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (when viewer
        (when-let [_ (:pdf/current @state/state)]
@@ -76,14 +77,15 @@
   (let [el-ref   (rum/use-ref nil)
         adjust-main-size!
         (util/debounce
-         200 (fn [width]
-               (let [root-el js/document.documentElement]
-                 (.setProperty (.-style root-el) "--ph-view-container-width" width)
-                 (pdf-utils/adjust-viewer-size! viewer))))
+         (fn [width]
+           (let [root-el js/document.documentElement]
+             (.setProperty (.-style root-el) "--ph-view-container-width" width)
+             (pdf-utils/adjust-viewer-size! viewer)))
+         200)
         group-id (.-$groupIdentity viewer)]
 
     ;; draggable handler
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
          (-> (js/interact el)
@@ -114,7 +116,7 @@
    {:keys [highlight point ^js selection]}
    {:keys [clear-ctx-menu! add-hl! upd-hl! del-hl!]}]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [cb  #(clear-ctx-menu!)
            doc (pdf-windows/resolve-own-document viewer)]
@@ -144,7 +146,8 @@
         action-fn! (fn [action clear?]
                      (when-let [action (and action (name action))]
                        (let [highlight (if (fn? highlight) (highlight) highlight)
-                             content (:content highlight)]
+                             content (:content highlight)
+                             ^js owner-win (pdf-windows/resolve-own-window viewer)]
                          (case action
                            "ref"
                            (pdf-assets/copy-hl-ref! highlight viewer)
@@ -153,7 +156,7 @@
                            (do
                              (util/copy-to-clipboard!
                               (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection)))
-                              :owner-window (pdf-windows/resolve-own-window viewer))
+                              :owner-window owner-win)
                              (pdf-utils/clear-all-selection))
 
                            "link"
@@ -176,7 +179,7 @@
                                                       {:id         (pdf-utils/gen-uuid)
                                                        :properties properties})]
                                  (p/let [highlight' (add-hl! highlight)]
-                                   (pdf-utils/clear-all-selection)
+                                   (pdf-utils/clear-all-selection owner-win)
                                    (pdf-assets/copy-hl-ref! highlight' viewer)))
 
 ;; update highlight
@@ -186,7 +189,7 @@
 
                        (and clear? (js/setTimeout #(clear-ctx-menu!) 68))))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (if new-&-highlight-mode?
          ;; wait for selection cleared ...
@@ -290,13 +293,13 @@
                               (.setData dt "text/plain" (str "((" id "))"))))
         update-hl!        (fn [hl] (some-> (rum/deref *ops-ref) (:upd-hl!) (apply [hl])))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (rum/set-ref! *ops-ref ops))
      [ops])
 
     ;; resizable
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js el (rum/deref *el)
              ^js it (-> (js/interact el)
@@ -464,12 +467,12 @@
 
         disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
 
-        fn-move                 (rum/use-callback
+        fn-move                 (hooks/use-callback
                                  (fn [^js/MouseEvent e]
                                    (set-end! (calc-coords! (.-pageX e) (.-pageY e))))
                                  [])]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js/HTMLElement root cnt-el]
          (let [fn-start (fn [^js/MouseEvent e]
@@ -548,7 +551,7 @@
         [highlights, set-highlights!] (rum/use-state initial-hls)
         [ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
 
-        clear-ctx-menu! (rum/use-callback
+        clear-ctx-menu! (hooks/use-callback
                          #(let [reset-fn (:reset-fn ctx-menu-state)]
                             (set-ctx-menu-state! {})
                             (and (fn? reset-fn) (reset-fn)))
@@ -590,7 +593,7 @@
                            (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
 
     ;; consume dirtied
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (if (rum/deref *mounted)
          (set-dirty-hls! highlights)
@@ -598,7 +601,7 @@
      [highlights])
 
     ;; selection events
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [fn-selection-ok
              (fn [^js/MouseEvent e]
@@ -653,7 +656,7 @@
      [viewer])
 
     ;; selection context menu
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js/Range sel-range (and (not (:collapsed sel-state)) (:range sel-state))]
          (let [^js point               (:point sel-state)
@@ -680,7 +683,7 @@
      [(:range sel-state)])
 
     ;; render hls
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
          (doseq [page loaded-pages]
@@ -741,7 +744,7 @@
         [area-dashed?, _set-area-dashed?] (use-atom *area-dashed?)]
 
     ;; instant pdfjs viewer
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js event-bus    (js/pdfjsViewer.EventBus.)
              ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
@@ -792,7 +795,7 @@
      [])
 
     ;; update window title
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js viewer (:viewer state)]
          (when (pdf-windows/check-viewer-in-system-win? viewer)
@@ -801,7 +804,7 @@
      [(:viewer state)])
 
     ;; interaction events
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js viewer (:viewer state)]
          (let [fn-textlayer-ready
@@ -894,7 +897,7 @@
 
     ;; current pdf effects
     (when-not db-based?
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (when pdf-current
            (pdf-assets/file-based-ensure-ref-page! pdf-current)))
@@ -902,7 +905,7 @@
 
     ;; load highlights
     (if db-based?
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (when pdf-current
            (let [pdf-block (:block pdf-current)]
@@ -913,7 +916,7 @@
                                    1))
                (set-hls-state! {:initial-hls highlights :latest-hls highlights :loaded true})))))
        [pdf-current])
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (p/catch
           (p/let [data (pdf-assets/file-based-load-hls-data$ pdf-current)
@@ -939,13 +942,14 @@
     ;; cache highlights
     (when-not db-based?
       (let [persist-hls-data!
-            (rum/use-callback
+            (hooks/use-callback
              (util/debounce
-              4000 (fn [latest-hls extra]
-                     (pdf-assets/file-based-persist-hls-data$
-                      pdf-current latest-hls extra))) [pdf-current])]
+              (fn [latest-hls extra]
+                (pdf-assets/file-based-persist-hls-data$
+                 pdf-current latest-hls extra))
+              4000) [pdf-current])]
 
-        (rum/use-effect!
+        (hooks/use-effect!
          (fn []
            (when (= :completed (:status loader-state))
              (p/catch
@@ -959,7 +963,7 @@
          [(:latest-hls hls-state) (:extra hls-state)])))
 
     ;; load document
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js loader-el (rum/deref *doc-ref)
              get-doc$      (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
@@ -973,13 +977,13 @@
          (set-loader-state! {:status :loading})
 
          (-> (get-doc$ (clj->js opts))
-           (p/then (fn [doc]
-                     (set-loader-state! {:pdf-document doc :status :completed})))
-           (p/catch #(set-loader-state! {:error %})))
+             (p/then (fn [doc]
+                       (set-loader-state! {:pdf-document doc :status :completed})))
+             (p/catch #(set-loader-state! {:error %})))
          #()))
      [url doc-password])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [error (:error loader-state)]
          (js/console.error "[PDF loader]" (:error loader-state))
@@ -1056,7 +1060,7 @@
         [ready set-ready!] (rum/use-state false)]
 
     ;; load assets
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (p/then
         (pdf-utils/load-base-assets$)
@@ -1064,7 +1068,7 @@
      [])
 
     ;; refresh loader
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (js/setTimeout #(set-ready! true) 100)
        #(set-ready! false))
@@ -1078,7 +1082,7 @@
 (rum/defc playground-effects
   [active]
 
-  (rum/use-effect!
+  (hooks/use-effect!
    (fn []
      (let [flg     "is-pdf-active"
            ^js cls (.-classList js/document.body)]

+ 34 - 33
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -2,23 +2,24 @@
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
             [frontend.components.svg :as svg]
+            [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
+            [frontend.db.async :as db-async]
+            [frontend.db.conn :as conn]
             [frontend.db.model :as db-model]
+            [frontend.db.utils :as db-utils]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.extensions.pdf.windows :refer [resolve-own-container] :as pdf-windows]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.notification :as notification]
-            [frontend.config :as config]
-            [frontend.db.conn :as conn]
-            [logseq.publishing.db :as publish-db]
-            [frontend.db.utils :as db-utils]
-            [frontend.db.async :as db-async]
+            [frontend.hooks :as hooks]
             [frontend.rum :refer [use-atom]]
             [frontend.state :as state]
             [frontend.storage :as storage]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [logseq.publishing.db :as publish-db]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [rum.core :as rum]))
@@ -39,7 +40,7 @@
         [hl-block-colored? set-hl-block-colored?] (rum/use-state (state/sub :pdf/block-highlight-colored?))
         [auto-open-ctx-menu? set-auto-open-ctx-menu!] (rum/use-state (state/sub :pdf/auto-open-ctx-menu?))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [el-popup (rum/deref *el-popup)
              cb       (fn [^js e]
@@ -50,26 +51,26 @@
          #(.removeEventListener el-popup "keyup" cb)))
      [])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (storage/set "ls-pdf-area-is-dashed" (boolean area-dashed?)))
      [area-dashed?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [b (boolean hl-block-colored?)]
          (state/set-state! :pdf/block-highlight-colored? b)
          (storage/set "ls-pdf-hl-block-is-colored" b)))
      [hl-block-colored?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [b (boolean auto-open-ctx-menu?)]
          (state/set-state! :pdf/auto-open-ctx-menu? b)
          (storage/set "ls-pdf-auto-open-ctx-menu" b)))
      [auto-open-ctx-menu?])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [cb  #(let [^js target (.-target %)]
                     (when (and (not (some-> (rum/deref *el-popup) (.contains target)))
@@ -181,7 +182,7 @@
                                           :findPrevious    prev?
                                           :matchDiacritics false})))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js doc (resolve-own-container viewer)]
          (let [handler (fn [^js e]
@@ -194,7 +195,7 @@
            #(.removeEventListener doc "click" handler))))
      [viewer])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js bus (.-eventBus viewer)]
          (.on bus "updatefindmatchescount" (fn [^js e]
@@ -209,7 +210,7 @@
                                                (bean/->clj (.-matchesCount e))))))))
      [viewer])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-not (nil? case-sensitive?)
          (do-find! :casesensitivitychange)))
@@ -331,12 +332,12 @@
   (when-let [^js pdf-doc (and viewer (.-pdfDocument viewer))]
     (let [*el-outline       (rum/use-ref nil)
           [outline-data, set-outline-data!] (rum/use-state [])
-          upt-outline-node! (rum/use-callback
+          upt-outline-node! (hooks/use-callback
                              (fn [path attrs]
                                (set-outline-data! (update-in outline-data path merge attrs)))
                              [outline-data])]
 
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (p/catch
           (p/let [^js data (.getOutline pdf-doc)]
@@ -351,7 +352,7 @@
             (js/console.error "[Load outline Error]" e))))
        [pdf-doc])
 
-      (rum/use-effect!
+      (hooks/use-effect!
        (fn []
          (let [el-outline (rum/deref *el-outline)
                cb         (fn [^js e]
@@ -380,17 +381,17 @@
 (rum/defc area-image-for-db
   [repo id]
   (let [[src set-src!] (rum/use-state nil)]
-    (rum/use-effect!
-      (fn []
-        (p/let [_ (db-async/<get-block repo id {:children? false})
-                block (db-model/get-block-by-uuid id)]
-          (when-let [asset-path' (and block (publish-db/get-area-block-asset-url
-                                              (conn/get-db (state/get-current-repo))
-                                              block
-                                              (db-utils/pull (:db/id (:block/page block)))))]
-            (-> asset-path' (assets-handler/<make-asset-url)
-              (p/then #(set-src! %))))))
-      [])
+    (hooks/use-effect!
+     (fn []
+       (p/let [_ (db-async/<get-block repo id {:children? false})
+               block (db-model/get-block-by-uuid id)]
+         (when-let [asset-path' (and block (publish-db/get-area-block-asset-url
+                                            (conn/get-db (state/get-current-repo))
+                                            block
+                                            (db-utils/pull (:db/id (:block/page block)))))]
+           (-> asset-path' (assets-handler/<make-asset-url)
+               (p/then #(set-src! %))))))
+     [])
 
     (when (string? src)
       [:p.area-wrap [:img {:src src}]])))
@@ -429,7 +430,7 @@
              (if db-graph?
                (area-image-for-db repo id)
                (let [fpath (pdf-assets/resolve-area-image-file
-                             img-stamp (state/get-current-pdf) hl)
+                            img-stamp (state/get-current-pdf) hl)
                      fpath (assets-handler/<make-asset-url fpath)]
                  [:p.area-wrap
                   [:img {:src fpath}]]))
@@ -442,7 +443,7 @@
         set-outline-visible! #(set-active-tab! "contents")
         contents?            (= active-tab "contents")]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js doc (resolve-own-container viewer)]
          (let [cb (fn [^js e]
@@ -491,7 +492,7 @@
         doc               (pdf-windows/resolve-own-document viewer)]
 
     ;; themes hooks
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js el (some-> doc (.getElementById (str "pdf-layout-container_" group-id)))]
          (set! (. (. el -dataset) -theme) viewer-theme)
@@ -500,7 +501,7 @@
      [viewer-theme])
 
     ;; export page state
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when viewer
          (.dispatch (.-eventBus viewer) (name :ls-update-extra-state)
@@ -508,7 +509,7 @@
      [viewer current-page-num])
 
     ;; pager hooks
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [total (and viewer (.-numPages (.-pdfDocument viewer)))]
          (let [^js bus (.-eventBus viewer)
@@ -522,7 +523,7 @@
            #(.off bus "pagechanging" page-fn))))
      [viewer])
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [^js input (rum/deref *page-ref)]
          (set! (. input -value) current-page-num)))

+ 5 - 3
src/main/frontend/extensions/pdf/utils.cljs

@@ -105,12 +105,14 @@
           (js/console.error e)))))))
 
 (defn clear-all-selection
-  []
-  (.removeAllRanges (js/window.getSelection)))
+  ([] (clear-all-selection js/window))
+  ([^js win]
+   (some-> win (.getSelection) (.removeAllRanges))))
 
 (def adjust-viewer-size!
   (util/debounce
-   200 (fn [^js viewer] (set! (. viewer -currentScaleValue) "auto"))))
+   (fn [^js viewer] (set! (. viewer -currentScaleValue) "auto"))
+   200))
 
 (defn fix-nested-js
   [its]

+ 5 - 4
src/main/frontend/extensions/pdf/windows.cljs

@@ -1,8 +1,8 @@
 (ns frontend.extensions.pdf.windows
-  (:require [frontend.state :as state]
-            [rum.core :as rum]
-            [cljs-bean.core :as bean]
-            [frontend.storage :as storage]))
+  (:require [cljs-bean.core :as bean]
+            [frontend.state :as state]
+            [frontend.storage :as storage]
+            [rum.core :as rum]))
 
 (def *active-win (atom nil))
 (def *exit-pending? (atom false))
@@ -93,6 +93,7 @@
                   (.appendChild (.-head doc) base)
                   (set! (.-title doc) (or (:filename pdf-current) "Logseq"))
                   (set! (.-dataset doc-el) -theme (str theme-mode))
+                  (set! (.-dataset doc-el) -color (or (some-> (state/sub :ui/radix-color) (name)) "logseq"))
                   (resolve-classes! doc)
                   (resolve-styles! doc)
                   (.appendChild (.-body doc) main)

+ 17 - 16
src/main/frontend/extensions/tldraw.cljs

@@ -1,35 +1,36 @@
 (ns frontend.extensions.tldraw
   "Adapters related to tldraw"
   (:require ["/frontend/tldraw-logseq" :as TldrawLogseq]
+            [cljs-bean.core :as bean]
             [frontend.components.block :as block]
             [frontend.components.export :as export]
             [frontend.components.page :as page]
+            [frontend.components.whiteboard :as whiteboard]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
-            [frontend.db.model :as model]
             [frontend.db :as db]
+            [frontend.db.async :as db-async]
+            [frontend.db.model :as model]
             [frontend.extensions.pdf.assets :as pdf-assets]
+            [frontend.handler.assets :as assets-handler]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.history :as history]
+            [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
-            [frontend.handler.assets :as assets-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
-            [frontend.handler.history :as history]
-            [frontend.handler.notification :as notification]
+            [frontend.hooks :as hooks]
             [frontend.rum :as r]
             [frontend.search :as search]
             [frontend.state :as state]
+            [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.util.text :as text-util]
             [goog.object :as gobj]
-            [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.ui :as ui]
-            [frontend.components.whiteboard :as whiteboard]
-            [cljs-bean.core :as bean]
-            [frontend.db.async :as db-async]
             [logseq.common.util :as common-util]
             [logseq.shui.ui :as shui]
-            [frontend.util.text :as text-util]))
+            [promesa.core :as p]
+            [rum.core :as rum]))
 
 (def tldraw (r/adapt-class (gobj/get TldrawLogseq "App")))
 
@@ -258,9 +259,9 @@
   [page-uuid block-id]
   (let [page-uuid (str page-uuid)
         [loaded-app set-loaded-app] (rum/use-state nil)]
-    (rum/use-effect! (fn []
-                       (when (and loaded-app block-id)
-                         (state/focus-whiteboard-shape loaded-app block-id))
-                       #())
-                     [block-id loaded-app])
+    (hooks/use-effect! (fn []
+                         (when (and loaded-app block-id)
+                           (state/focus-whiteboard-shape loaded-app block-id))
+                         #())
+                       [block-id loaded-app])
     (tldraw-app-inner page-uuid block-id loaded-app set-loaded-app)))

+ 2 - 1
src/main/frontend/extensions/zotero.cljs

@@ -9,6 +9,7 @@
             [frontend.extensions.zotero.setting :as setting]
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
+            [frontend.hooks :as hooks]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.util :as util]
@@ -67,7 +68,7 @@
 
                         (set-is-searching! false))))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [d-chan (chan)]
          (a/tap debounce-chan-mult d-chan)

+ 9 - 9
src/main/frontend/handler/common.cljs

@@ -1,16 +1,16 @@
 (ns frontend.handler.common
   "Common fns for handlers"
-  (:require [cljs-bean.core :as bean]
+  (:require ["ignore" :as Ignore]
+            [cljs-bean.core :as bean]
             [cljs.reader :as reader]
             [frontend.date :as date]
+            [frontend.db :as db]
+            [frontend.handler.property :as property-handler]
             [frontend.state :as state]
             [frontend.util :as util]
-            [frontend.handler.property :as property-handler]
-            [goog.object :as gobj]
             [goog.dom :as gdom]
-            ["ignore" :as Ignore]
             [goog.functions :refer [debounce]]
-            [frontend.db :as db]))
+            [goog.object :as gobj]))
 
 (defn copy-to-clipboard-without-id-property!
   [repo format raw-text html blocks]
@@ -47,21 +47,21 @@
   [pages]
   (map (fn [{:block/keys [created-at updated-at journal-day] :as p}]
          (cond->
-           p
+          p
 
            (nil? created-at)
            (assoc :block/created-at
                   (if journal-day
-                    (date/journal-day->ts journal-day)
+                    (date/journal-day->utc-ms journal-day)
                     (util/time-ms)))
 
            (nil? updated-at)
            (assoc :block/updated-at
                   ;; Not exact true
                   (if journal-day
-                    (date/journal-day->ts journal-day)
+                    (date/journal-day->utc-ms journal-day)
                     (util/time-ms)))))
-    pages))
+       pages))
 
 (defn listen-to-scroll!
   [element]

+ 25 - 24
src/main/frontend/handler/common/plugin.cljs

@@ -37,30 +37,31 @@
 
 (defn async-install-or-update-for-web!
   [{:keys [version repo only-check] :as manifest}]
-  (js/console.log "[plugin]" (if only-check "Checking" "Installing") " #" repo)
-  (-> (fetch-web-plugin-entry-info repo (if only-check "" version))
-      (p/then (fn [web-pkg]
-                (let [web-pkg (merge web-pkg (dissoc manifest :stat))
-                      latest-version (:version web-pkg)
-                      valid-latest-version (when only-check
-                                             (let [coerced-current-version (.coerce util/sem-ver version)
-                                                   coerced-latest-version (.coerce util/sem-ver latest-version)]
-                                               (if (and coerced-current-version
-                                                        coerced-latest-version
-                                                        (util/sem-ver.lt coerced-current-version coerced-latest-version))
-                                                 latest-version
-                                                 (throw (js/Error. :no-new-version)))))]
-                  (emit-lsp-updates!
-                   {:status :completed
-                    :only-check only-check
-                    :payload (if only-check
-                               (assoc manifest :latest-version valid-latest-version  :latest-notes "TODO: update notes")
-                               (assoc manifest :dst repo :version latest-version :web-pkg web-pkg))}))))
-      (p/catch (fn [^js e]
-                 (emit-lsp-updates!
-                  {:status :error
-                   :only-check only-check
-                   :payload (assoc manifest :error-code (.-message e))})))))
+  (js/console.log "debug:plugin:" (if only-check "Checking" "Installing") " #" repo)
+  (let [version (if (not only-check) (:latest-version manifest) version)]
+    (-> (fetch-web-plugin-entry-info repo (if only-check "" version))
+        (p/then (fn [web-pkg]
+                  (let [web-pkg (merge web-pkg (dissoc manifest :stat :version :only-check))
+                        latest-version (:version web-pkg)
+                        valid-latest-version (when only-check
+                                               (let [coerced-current-version (.coerce util/sem-ver version)
+                                                     coerced-latest-version (.coerce util/sem-ver latest-version)]
+                                                 (if (and coerced-current-version
+                                                          coerced-latest-version
+                                                          (util/sem-ver.lt coerced-current-version coerced-latest-version))
+                                                   latest-version
+                                                   (throw (js/Error. :no-new-version)))))]
+                    (emit-lsp-updates!
+                     {:status :completed
+                      :only-check only-check
+                      :payload (if only-check
+                                 (assoc manifest :latest-version valid-latest-version :latest-notes (some-> web-pkg :_objectExtra :releaseNotes))
+                                 (assoc manifest :dst repo :version latest-version :web-pkg web-pkg))}))))
+        (p/catch (fn [^js e]
+                   (emit-lsp-updates!
+                    {:status :error
+                     :only-check only-check
+                     :payload (assoc manifest :error-code (.-message e))}))))))
 
 (defn install-marketplace-plugin!
   "Installs plugin given plugin map with id"

+ 3 - 3
src/main/frontend/handler/editor.cljs

@@ -605,9 +605,9 @@
       (when-let [page (db/get-page page-title)]
         (let [class-or-property? (or (ldb/class? page) (ldb/property? page))]
           (when (or class-or-property? (db/page-empty? (state/get-current-repo) (:db/id page)))
-            (let [format (get page :block/format :markdown)
-                  new-block {:block/title ""
-                             :block/format format}]
+            (let [new-block (cond-> {:block/title ""}
+                              (not (config/db-based-graph? (state/get-current-repo)))
+                              (assoc :block/format (get page :block/format :markdown)))]
               (ui-outliner-tx/transact!
                {:outliner-op :insert-blocks}
                (outliner-op/insert-blocks! [new-block] page {:sibling? false})))))))))

+ 5 - 4
src/main/frontend/handler/events.cljs

@@ -39,6 +39,7 @@
             [frontend.fs.nfs :as nfs]
             [frontend.fs.sync :as sync]
             [frontend.fs.watcher-handler :as fs-watcher]
+            [frontend.handler.assets :as assets-handler]
             [frontend.handler.code :as code-handler]
             [frontend.handler.common.page :as page-common-handler]
             [frontend.handler.db-based.property :as db-property-handler]
@@ -57,7 +58,6 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.search :as search-handler]
             [frontend.handler.shell :as shell-handler]
-            [frontend.handler.assets :as assets-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
             [frontend.mobile.core :as mobile]
@@ -247,8 +247,9 @@
                (p/catch (fn [^js e]
                           (notification/show! (str e) :error)
                           (js/console.error e)))))))))
-    (shui/dialog-open!
-     (file-sync/pick-dest-to-sync-panel graph))))
+    (when (:GraphName graph)
+      (shui/dialog-open!
+       (file-sync/pick-dest-to-sync-panel graph)))))
 
 (defmethod handle :graph/pick-page-histories [[_ graph-uuid page-name]]
   (shui/dialog-open!
@@ -419,7 +420,7 @@
 
 (defmethod handle :go/proxy-settings [[_ agent-opts]]
   (shui/dialog-open!
-   (plugin/user-proxy-settings-panel agent-opts)
+   (plugin/user-proxy-settings-container agent-opts)
    {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
 
 (defmethod handle :redirect-to-home [_]

+ 25 - 24
src/main/frontend/handler/import.cljs

@@ -1,34 +1,34 @@
 (ns frontend.handler.import
   "Fns related to import from external services"
-  (:require [clojure.edn :as edn]
+  (:require [cljs.core.async.interop :refer [p->c]]
+            [clojure.core.async :as async]
+            [clojure.edn :as edn]
+            [clojure.string :as string]
             [clojure.walk :as walk]
-            [frontend.external :as external]
-            [frontend.handler.file :as file-handler]
-            [frontend.handler.repo :as repo-handler]
-            [frontend.handler.file-based.repo :as file-repo-handler]
-            [frontend.state :as state]
-            [frontend.date :as date]
             [frontend.config :as config]
-            [clojure.string :as string]
+            [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.format.mldoc :as mldoc]
+            [frontend.db.async :as db-async]
+            [frontend.external :as external]
             [frontend.format.block :as block]
-            [logseq.graph-parser.mldoc :as gp-mldoc]
-            [logseq.common.util :as common-util]
-            [logseq.graph-parser.whiteboard :as gp-whiteboard]
-            [logseq.common.util.date-time :as date-time-util]
-            [frontend.handler.page :as page-handler]
+            [frontend.format.mldoc :as mldoc]
             [frontend.handler.editor :as editor]
+            [frontend.handler.file :as file-handler]
+            [frontend.handler.file-based.repo :as file-repo-handler]
             [frontend.handler.notification :as notification]
-            [frontend.util :as util]
-            [clojure.core.async :as async]
-            [cljs.core.async.interop :refer [p->c]]
-            [medley.core :as medley]
+            [frontend.handler.page :as page-handler]
+            [frontend.handler.repo :as repo-handler]
             [frontend.persist-db :as persist-db]
-            [promesa.core :as p]
-            [frontend.db.async :as db-async]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [logseq.common.util :as common-util]
+            [logseq.common.util.date-time :as date-time-util]
+            [logseq.db :as ldb]
             [logseq.db.sqlite.util :as sqlite-util]
-            [logseq.db :as ldb]))
+            [logseq.graph-parser.mldoc :as gp-mldoc]
+            [logseq.graph-parser.whiteboard :as gp-whiteboard]
+            [medley.core :as medley]
+            [promesa.core :as p]))
 
 (defn index-files!
   "Create file structure, then parse into DB (client only)"
@@ -234,7 +234,7 @@
      (p/do!
       (persist-db/<import-db graph buffer)
       (state/add-repo! {:url graph})
-      (repo-handler/restore-and-setup-repo! graph {:import-type "sqlite"})
+      (repo-handler/restore-and-setup-repo! graph {:import-type :sqlite-db})
       (state/set-current-repo! graph)
       (persist-db/<export-db graph {})
       (db/transact! graph (sqlite-util/import-tx :sqlite-db))
@@ -298,9 +298,10 @@
   (let [graph (str config/db-version-prefix bare-graph-name)
         datoms (ldb/read-transit-str raw)]
     (p/do!
-     (persist-db/<new graph {:import-type "debug-transit"
+     (persist-db/<new graph {:import-type :debug-transit
                              :datoms datoms})
      (state/add-repo! {:url graph})
-     (repo-handler/restore-and-setup-repo! graph {:import-type "debug-transit"})
+     (repo-handler/restore-and-setup-repo! graph {:import-type :debug-transit})
+     (db/transact! graph (sqlite-util/import-tx :debug-transit))
      (state/set-current-repo! graph)
      (finished-ok-handler nil))))

+ 39 - 0
src/main/frontend/handler/plugin.cljs

@@ -773,6 +773,45 @@
     (when config/lsp-enabled?
       (hook-plugin-app (str :after-command-invoked type) nil))))
 
+(defn load-plugin-from-web-url!
+  [url]
+  (if (not (and (string? url) (string/starts-with? url "http")))
+    (p/rejected (js/Error. "Invalid web url"))
+    (p/let [url (string/replace url #"/+$" "")
+            github? (string/includes? url "github.com")
+            github-repo (when github?
+                          (some-> (re-find #"github.com/([^/]+/[^/]+)" url) (last)))
+            package-url (if github?
+                          (some-> github-repo
+                                  (plugin-common-handler/get-web-plugin-checker-url!))
+                          (str url "/package.json"))
+            ^js res (js/window.fetch (str package-url "?v=" (js/Date.now)))
+            package (if (and (.-ok res)
+                             (= (.-status res) 200))
+                      (-> (.json res)
+                          (p/then bean/->clj))
+                      (throw (js/Error. (.text res))))
+            logseq (or (:logseq package)
+                       (throw (js/Error. "Illegal logseq package")))]
+      (let [id (if github?
+                 (some-> github-repo (string/replace "/" "_"))
+                 (or (:id logseq) (:name package)))
+            repo (or github-repo id)
+            theme? (some? (or (:theme logseq) (:themes logseq)))]
+
+        (plugin-common-handler/emit-lsp-updates!
+         {:status :completed
+          :only-check false
+          :payload {:id id
+                    :repo repo
+                    :dst repo
+                    :theme theme?
+                    :web-pkg (cond-> package
+
+                               (not github?)
+                               (assoc :installedFromUserWebUrl url))}}))
+      url)))
+
 ;; components
 (rum/defc lsp-indicator < rum/reactive
   []

+ 23 - 11
src/main/frontend/handler/profiler.cljs

@@ -1,7 +1,8 @@
 (ns frontend.handler.profiler
   "Provides fns for profiling.
   TODO: support both main thread and worker thread."
-  (:require [goog.object :as g]))
+  (:require [clojure.string :as string]
+            [goog.object :as g]))
 
 (def ^:private *fn-symbol->key->call-count (volatile! {}))
 (def ^:private *fn-symbol->key->time-sum (volatile! {}))
@@ -9,28 +10,32 @@
 (def *fn-symbol->origin-fn (atom {}))
 
 (defn- get-profile-fn
-  [fn-sym original-fn custom-key-fn]
+  [fn-sym original-fn]
   (fn profile-fn-inner [& args]
     (let [start (system-time)
           r (apply original-fn args)
-          elapsed-time (- (system-time) start)
-          k (when custom-key-fn (custom-key-fn r))]
+          elapsed-time (- (system-time) start)]
       (vswap! *fn-symbol->key->call-count update-in [fn-sym :total] inc)
       (vswap! *fn-symbol->key->time-sum update-in [fn-sym :total] #(+ % elapsed-time))
-      (when k
-        (vswap! *fn-symbol->key->call-count update-in [fn-sym k] inc)
-        (vswap! *fn-symbol->key->time-sum update-in [fn-sym k] #(+ % elapsed-time)))
       r)))
 
+(defn- replace-fn-helper!
+  [ns munged-name fn-sym original-fn-obj]
+  (let [ns-obj (find-ns-obj ns)
+        obj-cljs-keys (filter #(string/starts-with? % "cljs$") (js-keys original-fn-obj))]
+    (g/set ns-obj munged-name (get-profile-fn fn-sym original-fn-obj))
+    (let [new-obj (find-ns-obj (str ns "." munged-name))]
+      (doseq [k obj-cljs-keys]
+        (g/set new-obj k (g/get original-fn-obj k))))))
+
 (defn register-fn!
-  [fn-sym & {:keys [custom-key-fn] :as _opts}]
+  [fn-sym & {:as _opts}]
   (assert (qualified-symbol? fn-sym))
   (let [ns (namespace fn-sym)
         s (munge (name fn-sym))]
     (if-let [original-fn (find-ns-obj (str ns "." s))]
-      (let [profiled-fn (get-profile-fn fn-sym original-fn custom-key-fn)]
-        (swap! *fn-symbol->origin-fn assoc fn-sym original-fn)
-        (g/set (find-ns-obj ns) s profiled-fn))
+      (do (replace-fn-helper! ns s fn-sym original-fn)
+          (swap! *fn-symbol->origin-fn assoc fn-sym original-fn))
       (throw (ex-info (str "fn-sym not found: " fn-sym) {})))))
 
 (defn unregister-fn!
@@ -60,3 +65,10 @@
   (pprint/pprint (profile-report))
   (reset-report!)
   (unregister-fn! 'datascript.core/entity))
+
+(comment
+  ;; test multi-arity, variadic fn
+  (defn test-fn-to-profile
+    ([] 1)
+    ([_a] 2)
+    ([_a & _args] 3)))

+ 55 - 0
src/main/frontend/hooks.cljs

@@ -0,0 +1,55 @@
+(ns frontend.hooks
+  "React custom hooks."
+  (:refer-clojure :exclude [ref deref])
+  (:require [goog.functions :as gfun]
+            [rum.core :as rum]))
+
+(defn- memo-deps
+  [equal-fn deps]
+  (let [equal-fn (or equal-fn =)
+        ^js deps-ref (rum/use-ref deps)]
+    (when-not (equal-fn (.-current deps-ref) deps)
+      (set! (.-current deps-ref) deps))
+    (.-current deps-ref)))
+
+#_{:clj-kondo/ignore [:discouraged-var]}
+(defn use-memo
+  [f deps & {:keys [equal-fn]}]
+  (rum/use-memo f #js[(memo-deps equal-fn deps)]))
+
+#_{:clj-kondo/ignore [:discouraged-var]}
+(defn use-effect!
+  "setup-fn will be invoked every render of component when no deps arg provided"
+  ([setup-fn] (rum/use-effect! setup-fn))
+  ([setup-fn deps & {:keys [equal-fn]}]
+   (rum/use-effect! setup-fn #js[(memo-deps equal-fn deps)])))
+
+#_{:clj-kondo/ignore [:discouraged-var]}
+(defn use-layout-effect!
+  ([setup-fn] (rum/use-layout-effect! setup-fn))
+  ([setup-fn deps & {:keys [equal-fn]}]
+   (rum/use-layout-effect! setup-fn #js[(memo-deps equal-fn deps)])))
+
+#_{:clj-kondo/ignore [:discouraged-var]}
+(defn use-callback
+  [callback deps & {:keys [equal-fn]}]
+  (rum/use-callback callback #js[(memo-deps equal-fn deps)]))
+
+;;; unchanged hooks, link to rum/use-xxx directly
+(def use-ref rum/use-ref)
+(def create-ref rum/create-ref)
+(def deref rum/deref)
+(def set-ref! rum/set-ref!)
+(def use-state rum/use-state)
+(comment
+  (def use-reducer rum/use-reducer))
+
+;;; other custom hooks
+
+(defn use-debounced-value
+  "Return the debounced value"
+  [value msec]
+  (let [[debounced-value set-value!] (use-state value)
+        cb (use-callback (gfun/debounce set-value! msec) [])]
+    (use-effect! #(cb value) [value])
+    debounced-value))

+ 3 - 12
src/main/frontend/mixins.cljs

@@ -1,10 +1,9 @@
 (ns frontend.mixins
   "Rum mixins for use in components"
-  (:require [rum.core :as rum]
-            [goog.dom :as dom]
+  (:require [frontend.state :as state]
             [frontend.util :refer [profile] :as util]
-            [frontend.state :as state]
-            [goog.functions :as gfun])
+            [goog.dom :as dom]
+            [rum.core :as rum])
   (:import [goog.events EventHandler]))
 
 (defn detach
@@ -147,11 +146,3 @@
        (profile
         (str "Render " desc)
         (render-fn state))))})
-
-(defn use-debounce
-  "A rumext custom hook that debounces the value changes"
-  [ms value]
-  (let [[state update-fn] (rum/use-state value)
-        update-fn (rum/use-callback (gfun/debounce update-fn ms) [])]
-    (rum/use-effect! #(update-fn value) #js [value])
-    state))

+ 23 - 22
src/main/frontend/mobile/graph_picker.cljs

@@ -1,20 +1,21 @@
 (ns frontend.mobile.graph-picker
   (:require
    [clojure.string :as string]
-   [logseq.shui.ui :as shui]
-   [rum.core :as rum]
-   [frontend.ui :as ui]
-   [frontend.handler.notification :as notification]
+   [frontend.components.svg :as svg]
+   [frontend.fs :as fs]
    [frontend.handler.file-based.nfs :as nfs-handler]
+   [frontend.handler.notification :as notification]
    [frontend.handler.page :as page-handler]
-   [frontend.util :as util]
+   [frontend.hooks :as hooks]
+   [frontend.mobile.util :as mobile-util]
    [frontend.modules.shortcut.core :as shortcut]
    [frontend.state :as state]
-   [frontend.mobile.util :as mobile-util]
-   [frontend.fs :as fs]
-   [frontend.components.svg :as svg]
+   [frontend.ui :as ui]
+   [frontend.util :as util]
+   [logseq.common.path :as path]
+   [logseq.shui.ui :as shui]
    [promesa.core :as p]
-   [logseq.common.path :as path]))
+   [rum.core :as rum]))
 
 (defn validate-graph-dirname
   [root dirname]
@@ -23,14 +24,14 @@
 (rum/defc toggle-item
   [{:keys [on? title on-toggle]}]
   (ui/button
-    [:span.flex.items-center.justify-between.w-full.py-1
-     [:strong title]
-     (ui/toggle on? (fn []) true)]
-    :class (str "toggle-item " (when on? "is-on"))
-    :intent "logseq"
-    :on-pointer-down #(util/stop %)
-    :on-click #(when (fn? on-toggle)
-                 (on-toggle (not on?)))))
+   [:span.flex.items-center.justify-between.w-full.py-1
+    [:strong title]
+    (ui/toggle on? (fn []) true)]
+   :class (str "toggle-item " (when on? "is-on"))
+   :intent "logseq"
+   :on-pointer-down #(util/stop %)
+   :on-click #(when (fn? on-toggle)
+                (on-toggle (not on?)))))
 
 (rum/defc ^:large-vars/cleanup-todo graph-picker-cp
   [{:keys [onboarding-and-home? logged? native-icloud?] :as opts}]
@@ -76,7 +77,7 @@
                                                   (notification/show! (str e) :error)
                                                   (js/console.error e)))))))))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (when-let [^js input (and onboarding-and-home?
                                  (rum/deref *input-ref))]
@@ -119,10 +120,10 @@
          :on-click (fn []
                      (shui/dialog-close!)
                      (page-handler/ls-dir-files! shortcut/refresh!
-                       {:dir (when native-ios?
-                               (or
-                                 (state/get-icloud-container-root-url)
-                                 (state/get-local-container-root-url)))})))]
+                                                 {:dir (when native-ios?
+                                                         (or
+                                                          (state/get-icloud-container-root-url)
+                                                          (state/get-local-container-root-url)))})))]
 
        ;; step 1
        :new-graph

+ 5 - 4
src/main/frontend/rum.cljs

@@ -5,7 +5,8 @@
             [clojure.string :as string]
             [clojure.walk :as w]
             [daiquiri.interpreter :as interpreter]
-            [rum.core :refer [use-state use-effect!] :as rum]))
+            [frontend.hooks :as hooks]
+            [rum.core :refer [use-state] :as rum]))
 
 ;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
 
@@ -68,7 +69,7 @@
 (defn use-atom-fn
   [a getter-fn setter-fn]
   (let [[val set-val] (use-state (getter-fn @a))]
-    (use-effect!
+    (hooks/use-effect!
      (fn []
        (let [id (str (random-uuid))]
          (add-watch a id (fn [_ _ prev-state next-state]
@@ -93,7 +94,7 @@
 (defn use-mounted
   []
   (let [*mounted (rum/use-ref false)]
-    (use-effect!
+    (hooks/use-effect!
      (fn []
        (rum/set-ref! *mounted true)
        #(rum/set-ref! *mounted false))
@@ -107,7 +108,7 @@
   ([tick]
    (let [[ref set-ref] (rum/use-state nil)
          [rect set-rect] (rum/use-state nil)]
-     (rum/use-effect!
+     (hooks/use-effect!
       (if ref
         (fn []
           (let [update-rect #(set-rect (. ref getBoundingClientRect))

+ 1 - 1
src/main/frontend/state.cljs

@@ -2035,7 +2035,7 @@ 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 %))
+    #(and (if include-unpacked? true (or (:webMode %) (: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)))))

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

@@ -1,22 +1,24 @@
 (ns frontend.ui
   "Main ns for reusable components"
-  (:require ["@logseq/react-tweet-embed" :as react-tweet-embed]
+  (:require ["@emoji-mart/data" :as emoji-data]
+            ["@logseq/react-tweet-embed" :as react-tweet-embed]
+            ["emoji-mart" :as emoji-mart]
             ["react-intersection-observer" :as react-intersection-observer]
             ["react-textarea-autosize" :as TextareaAutosize]
             ["react-tippy" :as react-tippy]
             ["react-transition-group" :refer [CSSTransition TransitionGroup]]
             ["react-virtuoso" :refer [Virtuoso VirtuosoGrid]]
-            ["@emoji-mart/data" :as emoji-data]
-            ["emoji-mart" :as emoji-mart]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
             [electron.ipc :as ipc]
             [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
+            [frontend.date :as date]
             [frontend.db-mixins :as db-mixins]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
+            [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.config :as shortcut-config]
@@ -32,11 +34,10 @@
             [lambdaisland.glogi :as log]
             [logseq.shui.icon.v2 :as shui.icon.v2]
             [logseq.shui.popup.core :as shui-popup]
+            [logseq.shui.ui :as shui]
             [medley.core :as medley]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [logseq.shui.ui :as shui]
-            [frontend.date :as date]))
+            [rum.core :as rum]))
 
 (declare icon)
 
@@ -368,7 +369,7 @@
                       input)))
         [time set-time] (rum/use-state (time-fn))]
 
-    (rum/use-effect!
+    (hooks/use-effect!
      (fn []
        (let [timer (js/setInterval
                     #(set-time (time-fn)) (* 1000 30))]
@@ -1160,7 +1161,7 @@
 (rum/defc indicator-progress-pie
   [percentage]
   (let [*el (rum/use-ref nil)]
-    (rum/use-effect!
+    (hooks/use-effect!
      #(when-let [^js el (rum/deref *el)]
         (set! (.. el -style -backgroundImage)
               (util/format "conic-gradient(var(--ls-pie-fg-color) %s%, var(--ls-pie-bg-color) %s%)" percentage percentage)))

+ 3 - 14
src/main/frontend/util.cljc

@@ -23,6 +23,7 @@
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [goog.string :as gstring]
+            [goog.functions :as gfun]
             [goog.userAgent]
             [promesa.core :as p]
             [rum.core :as rum]
@@ -307,20 +308,8 @@
        x)))
 
 #?(:cljs
-   (defn debounce
-     "Returns a function that will call f only after threshold has passed without new calls
-      to the function. Calls prep-fn on the args in a sync way, which can be used for things like
-      calling .persist on the event object to be able to access the event attributes in f"
-     ([threshold f] (debounce threshold f (constantly nil)))
-     ([threshold f prep-fn]
-      (let [t (atom nil)]
-        (fn [& args]
-          (when @t (js/clearTimeout @t))
-          (apply prep-fn args)
-          (reset! t (js/setTimeout #(do
-                                      (reset! t nil)
-                                      (apply f args))
-                                   threshold)))))))
+   (def debounce gfun/debounce))
+
 #?(:cljs
    (defn cancelable-debounce
      "Create a stateful debounce function with specified interval

+ 13 - 7
src/main/frontend/worker/commands.cljs

@@ -154,17 +154,23 @@
                            :logseq.task/scheduled)
         frequency (db-property/property-value-content (:logseq.task/recur-frequency entity))
         unit (:logseq.task/recur-unit entity)
-        current-value (get entity property-ident)]
+        property (d/entity db property-ident)
+        date? (= :date (get-in property [:block/schema :type]))
+        current-value (cond->
+                       (get entity property-ident)
+                        date?
+                        (#(date-time-util/journal-day->ms (:block/journal-day %))))]
     (when (and frequency unit)
       (when-let [next-time-long (get-next-time current-value unit frequency)]
         (let [journal-day (outliner-pipeline/get-journal-day-from-long db next-time-long)
-              create-journal-page (when-not journal-day
-                                    (let [formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))
-                                          title (date-time-util/format (t/to-default-time-zone (tc/to-date-time next-time-long)) formatter)]
-                                      (worker-db-page/create db title {:create-first-block? false})))
-              value next-time-long]
+              {:keys [tx-data page-uuid]} (if journal-day
+                                            {:page-uuid (:block/uuid (d/entity db journal-day))}
+                                            (let [formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))
+                                                  title (date-time-util/format (t/to-default-time-zone (tc/to-date-time next-time-long)) formatter)]
+                                              (worker-db-page/create db title {:create-first-block? false})))
+              value (if date? [:block/uuid page-uuid] next-time-long)]
           (concat
-           (:tx-data create-journal-page)
+           tx-data
            (when value
              [[:db/add (:db/id entity) property-ident value]])))))))
 

+ 161 - 32
src/main/frontend/worker/db/migrate.cljs

@@ -401,24 +401,26 @@
         datoms (d/datoms db :avet :block/title)]
     (keep
      (fn [{:keys [e v]}]
-       (when (string/includes? v ref-special-chars)
-         (let [entity (d/entity db e)]
-           (cond
-             (and (ldb/page? entity)
-                  (re-find db-content/id-ref-pattern v))
-             [:db/retractEntity e]
-
-             (string/includes? v (str page-ref/left-brackets ref-special-chars))
-             (let [title' (string/replace v (str page-ref/left-brackets ref-special-chars) page-ref/left-brackets)]
-               (prn :debug {:old-title v :new-title title'})
-               {:db/id e
-                :block/title title'})
-
-             (re-find id-ref-pattern v)
-             (let [title' (string/replace v id-ref-pattern "$1")]
-               (prn :debug {:old-title v :new-title title'})
-               {:db/id e
-                :block/title title'})))))
+       (if (string? v)
+         (when (string/includes? v ref-special-chars)
+           (let [entity (d/entity db e)]
+             (cond
+               (and (ldb/page? entity)
+                    (re-find db-content/id-ref-pattern v))
+               [:db/retractEntity e]
+
+               (string/includes? v (str page-ref/left-brackets ref-special-chars))
+               (let [title' (string/replace v (str page-ref/left-brackets ref-special-chars) page-ref/left-brackets)]
+                 (prn :debug {:old-title v :new-title title'})
+                 {:db/id e
+                  :block/title title'})
+
+               (re-find id-ref-pattern v)
+               (let [title' (string/replace v id-ref-pattern "$1")]
+                 (prn :debug {:old-title v :new-title title'})
+                 {:db/id e
+                  :block/title title'}))))
+         [:db/retractEntity e]))
      datoms)))
 
 (defn- replace-block-type-with-tags
@@ -474,8 +476,8 @@
               [:db/retract (:e d) :logseq.task/deadline]))
           datoms))))))
 
-(defn- remove-block-format-from-db
-  [conn _search-db]
+(defn- remove-block-format-from-db!
+  [conn]
   (let [db @conn]
     (when (ldb/db-based-graph? db)
       (let [datoms (d/datoms db :avet :block/uuid)
@@ -484,7 +486,33 @@
                        [:db/retract (:e d) :block/format])
                      datoms)]
         (ldb/transact! conn tx-data {:db-migrate? true})
-        (d/reset-schema! conn (dissoc (:schema db) :block/format))
+        (d/reset-schema! conn (dissoc (:schema db) :block/format))))))
+
+(defn- remove-duplicated-contents-page
+  [conn _search-db]
+  (let [db @conn]
+    (when (ldb/db-based-graph? db)
+      (let [duplicated-contents-pages (->>
+                                       (d/q
+                                        '[:find ?b ?created-at
+                                          :where
+                                          [?b :block/title "Contents"]
+                                          [?b :block/tags ?t]
+                                          [?t :db/ident :logseq.class/Page]
+                                          [?b :logseq.property/built-in? true]
+                                          [?b :block/created-at ?created-at]]
+                                        db)
+                                       (sort-by second)
+                                       rest)]
+        (when (seq duplicated-contents-pages)
+          (let [tx-data (mapcat
+                         (fn [[e _]]
+                           (let [p (d/entity db e)
+                                 blocks (:block/_page p)]
+                             (conj (mapv (fn [b] [:db/retractEntity (:db/id b)]) blocks)
+                                   [:db/retractEntity e])))
+                         duplicated-contents-pages)]
+            (ldb/transact! conn tx-data {:db-migrate? true})))
         []))))
 
 (defn- deprecate-logseq-user-ns
@@ -596,7 +624,8 @@
    [56 {:properties [:logseq.property/enable-history?
                      :logseq.property.history/block :logseq.property.history/property
                      :logseq.property.history/ref-value :logseq.property.history/scalar-value]}]
-   [57 {:fix remove-block-format-from-db}]])
+   [58 {:fix remove-duplicated-contents-page}]
+   [59 {:properties [:logseq.property/created-by]}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))
@@ -613,6 +642,9 @@
                               (= (:db/ident data) :logseq.kv/schema-version)
                               nil
 
+                              (= (:block/title data) "Contents")
+                              nil
+
                               (:file/path data)
                               (if-let [block (d/entity @conn [:file/path (:file/path data)])]
                                 (let [existing-data (assoc (into {} block) :db/id (:db/id block))]
@@ -703,9 +735,7 @@
                    [(when-not (:block/title entity)
                       [:db/add (:e d) :block/title (:v d)])
                     (when-not (:block/uuid entity)
-                      [:db/add (:e d) :block/uuid (d/squuid)])
-                    (when-not (:block/format entity)
-                      [:db/add (:e d) :block/format :markdown])]))
+                      [:db/add (:e d) :block/uuid (d/squuid)])]))
                (d/datoms @conn :avet :block/name))
               (remove nil?))]
     (when (seq data)
@@ -774,6 +804,7 @@
                               schema-version->updates)]
             (fix-path-refs! conn)
             (fix-missing-title! conn)
+            (remove-block-format-from-db! conn)
             (fix-properties! conn)
             (fix-block-timestamps! conn)
             (println "DB schema migrated from" version-in-db)
@@ -786,14 +817,112 @@
             (js/console.error e)
             (throw e)))))))
 
+(defn fix-invalid-data!
+  [conn invalid-entity-ids]
+  (let [db @conn
+        tx-data (->>
+                 (mapcat
+                  (fn [id]
+                    (let [entity (d/entity db id)
+                         ;; choice not included in closed values
+                          wrong-choice (keep
+                                        (fn [[k v]]
+                                          (if (= "block.temp" (namespace k))
+                                            [:db/retract (:db/id entity) k]
+                                            (when-let [property (d/entity db k)]
+                                              (let [closed-values (:property/closed-values property)]
+                                                (when (seq closed-values)
+                                                  (if (and (de/entity? v)
+                                                           (not (contains? (set (map :db/id closed-values)) (:db/id v))))
+                                                    [:db/retractEntity (:db/id v)]
+                                                    [:db/retract (:db/id entity) k]))))))
+                                        (into {} entity))
+                          eid (:db/id entity)
+                          fix (cond
+                                (= #{:block/tx-id} (set (keys entity)))
+                                [[:db/retractEntity (:db/id entity)]]
+
+                                (and (seq (:block/refs entity))
+                                     (not (or (:block/title entity) (:block/content entity) (:property.value/content entity))))
+                                [[:db/retractEntity (:db/id entity)]]
+
+                                (:logseq.property.node/type entity)
+                                [[:db/retract eid :logseq.property.node/type]
+                                 [:db/retractEntity :logseq.property.node/type]
+                                 [:db/add eid :logseq.property.node/display-type (:logseq.property.node/type entity)]]
+
+                                (and (:db/cardinality entity) (not (ldb/property? entity)))
+                                [[:db/add eid :block/tags :logseq.class/Property]
+                                 [:db/retract eid :block/tags :logseq.class/Page]]
+
+                                (when-let [schema (:block/schema entity)]
+                                  (or (:cardinality schema) (:classes schema)))
+                                (let [schema (:block/schema entity)]
+                                  [[:db/add eid :block/schema (dissoc schema :cardinality :classes)]])
+
+                                (and (:logseq.property.asset/type entity)
+                                     (or (nil? (:logseq.property.asset/checksum entity))
+                                         (nil? (:logseq.property.asset/size entity))))
+                                [[:db/retractEntity eid]]
+
+                                ;; add missing :db/ident for classes && properties
+                                (and (ldb/class? entity) (nil? (:db/ident entity)))
+                                [[:db/add (:db/id entity) :db/ident (db-class/create-user-class-ident-from-name (:block/title entity))]]
+
+                                (and (ldb/property? entity) (nil? (:db/ident entity)))
+                                [[:db/add (:db/id entity) :db/ident (db-property/create-user-property-ident-from-name (:block/title entity))]]
+
+                                ;; remove #Page for classes/properties/journals
+                                (and (ldb/internal-page? entity) (or (ldb/class? entity) (ldb/property? entity) (ldb/journal? entity)))
+                                [[:db/retract (:db/id entity) :block/tags :logseq.class/Page]]
+
+                                ;; remove file entities
+                                (and (:file/path entity)
+                                     (not (contains? #{"logseq/custom.css" "logseq/config.js"  "logseq/config.edn"} (:file/path entity))))
+                                [[:db/retractEntity (:db/id entity)]]
+
+                                (:block/properties-order entity)
+                                [[:db/retract (:db/id entity) :block/properties-order]]
+
+                                (:block/macros entity)
+                                [[:db/retract (:db/id entity) :block/macros]]
+
+                                (and (seq (:block/tags entity)) (not (every? ldb/class? (:block/tags entity))))
+                                (let [tags (remove ldb/class? (:block/tags entity))]
+                                  (map
+                                   (fn [tag]
+                                     {:db/id (:db/id tag)
+                                      :db/ident (or (:db/ident tag) (db-class/create-user-class-ident-from-name (:block/title entity)))
+                                      :block/tags :logseq.class/Tag})
+                                   tags))
+                                :else
+                                nil)]
+                      (into fix wrong-choice)))
+                  invalid-entity-ids)
+                 distinct)]
+    (when (seq tx-data)
+      (d/transact! conn tx-data {:fix-db? true}))))
+
 (defn fix-db!
-  [conn]
-  (ensure-built-in-data-exists! conn)
-  (fix-path-refs! conn)
-  (fix-missing-title! conn)
-  (fix-properties! conn)
-  (fix-block-timestamps! conn)
-  (fix-missing-page-tag! conn))
+  [conn & {:keys [invalid-entity-ids]}]
+  (when (ldb/db-based-graph? @conn)
+    (try
+      (ensure-built-in-data-exists! conn)
+      (remove-block-format-from-db! conn)
+      (fix-path-refs! conn)
+      (fix-missing-title! conn)
+      (fix-properties! conn)
+      (fix-block-timestamps! conn)
+      (fix-missing-page-tag! conn)
+      (when (seq invalid-entity-ids)
+        (fix-invalid-data! conn invalid-entity-ids))
+
+      ;; TODO: remove this after RTC db fixed
+      (let [data (deprecate-logseq-user-ns conn nil)]
+        (when (seq data)
+          (d/transact! conn data {:fix-db? true})))
+      (catch :default e
+        (js/console.error e)))))
 
 ;; Backend migrations
 ;; ==================

+ 7 - 6
src/main/frontend/worker/db/validate.cljs

@@ -1,7 +1,6 @@
 (ns frontend.worker.db.validate
   "Validate db"
-  (:require [cljs.pprint :as pprint]
-            [frontend.worker.util :as worker-util]
+  (:require [frontend.worker.util :as worker-util]
             [logseq.db.frontend.validate :as db-validate]))
 
 (defn validate-db
@@ -10,13 +9,15 @@
     (if errors
       (do
         (worker-util/post-message :log [:db-invalid :error
-                                        {:msg (str "Validation detected " (count errors) " invalid block(s):")
-                                         :counts (assoc (db-validate/graph-counts db entities) :datoms datom-count)}])
-        (pprint/pprint errors)
+                                        {:msg "Validation errors"
+                                         :errors errors}])
         (worker-util/post-message :notification
                                   [(str "Validation detected " (count errors) " invalid block(s). These blocks may be buggy when you interact with them. See the javascript console for more.")
                                    :warning false]))
 
       (worker-util/post-message :notification
                                 [(str "Your graph is valid! " (assoc (db-validate/graph-counts db entities) :datoms datom-count))
-                                 :success false]))))
+                                 :success false]))
+    {:errors errors
+     :datom-count datom-count
+     :invalid-entity-ids (distinct (map (fn [e] (:db/id (:entity e))) errors))}))

+ 0 - 16
src/main/frontend/worker/db_listener.cljs

@@ -2,7 +2,6 @@
   "Db listeners for worker-db."
   (:require [cljs-bean.core :as bean]
             [datascript.core :as d]
-            [frontend.common.schema-register :as sr]
             [frontend.worker.pipeline :as worker-pipeline]
             [frontend.worker.search :as search]
             [frontend.worker.state :as worker-state]
@@ -14,21 +13,6 @@
 (defmulti listen-db-changes
   (fn [listen-key & _] listen-key))
 
-(sr/defkeyword :gen-rtc-ops
-  "DB-listener key.
-generate rtc ops.")
-
-(sr/defkeyword :gen-undo-ops
-  "DB-listener key.
-generate undo ops.")
-
-(sr/defkeyword :gen-asset-change-events
-  "DB-listener key.
-generate asset-change events.")
-
-(sr/defkeyword :sync-db-to-main-thread
-  "")
-
 (defn- sync-db-to-main-thread
   "Return tx-report"
   [repo conn {:keys [tx-meta] :as tx-report}]

+ 3 - 1
src/main/frontend/worker/db_worker.cljs

@@ -912,7 +912,9 @@
   (validate-db
    [_this repo]
    (when-let [conn (worker-state/get-datascript-conn repo)]
-     (worker-db-validate/validate-db @conn)))
+     (let [result (worker-db-validate/validate-db @conn)]
+       (db-migrate/fix-db! conn {:invalid-entity-ids (:invalid-entity-ids result)})
+       result)))
 
   (dangerousRemoveAllDbs
    [this repo]

+ 5 - 2
src/main/frontend/worker/handler/page.cljs

@@ -86,8 +86,11 @@
                 delete-file-tx (when file
                                  [[:db.fn/retractEntity [:file/path file-path]]])
                 delete-property-tx (when (ldb/property? page)
-                                     (let [datoms (d/datoms @conn :avet (:db/ident page))]
-                                       (map (fn [d] [:db/retract (:e d) (:a d)]) datoms)))
+                                     (concat
+                                      (let [datoms (d/datoms @conn :avet (:db/ident page))]
+                                        (map (fn [d] [:db/retract (:e d) (:a d)]) datoms))
+                                      (map (fn [d] [:db/retractEntity (:e d)])
+                                           (d/datoms @conn :avet :logseq.property.history/property (:db/ident page)))))
                 delete-page-tx (concat (db-refs->page repo page)
                                        delete-property-tx
                                        [[:db.fn/retractEntity (:db/id page)]])

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

@@ -30,13 +30,16 @@
                                 (let [v (if (uuid? tag)
                                           (d/entity db [:block/uuid tag])
                                           tag)]
-                                  (cond
-                                    (de/entity? v)
-                                    (:db/id v)
-                                    (map? v)
-                                    (:db/id v)
-                                    :else
-                                    v)))
+                                  (cond (de/entity? v)
+                                        (:db/id v)
+                                        ;; tx map
+                                        (map? v)
+                                        ;; Handle adding :db/ident if a new tag
+                                        (if (d/entity db [:block/uuid (:block/uuid v)])
+                                          v
+                                          (db-class/build-new-class db v))
+                                        :else
+                                        v)))
                               tags'))
           property-vals-tx-m
           ;; Builds property values for built-in properties like logseq.property.pdf/file
@@ -49,9 +52,12 @@
                         (when (db-property-util/built-in-has-ref-value? k)
                           [k v])))
                 (into {})))]
-      (cond-> (if class? [(db-class/build-new-class db page')
-                          [:db/retract [:block/uuid (:block/uuid page)] :block/tags :logseq.class/Page]]
-                  [page'])
+      (cond-> (if class?
+                [(merge (db-class/build-new-class db page')
+                        ;; FIXME: new pages shouldn't have db/ident but converting property to tag still relies on this
+                        (select-keys page' [:db/ident]))
+                 [:db/retract [:block/uuid (:block/uuid page)] :block/tags :logseq.class/Page]]
+                [page'])
         (seq property-vals-tx-m)
         (into (vals property-vals-tx-m))
         true
@@ -187,7 +193,8 @@
                    (not (ldb/class? existing-page))
                    (or (ldb/property? existing-page) (ldb/internal-page? existing-page)))
           ;; Convert existing user property or page to class
-          (let [tx-data [(db-class/build-new-class db (select-keys existing-page [:block/title :block/uuid :db/ident :block/created-at]))
+          (let [tx-data [(merge (db-class/build-new-class db (select-keys existing-page [:block/title :block/uuid :block/created-at]))
+                                (select-keys existing-page [:db/ident]))
                          [:db/retract [:block/uuid (:block/uuid existing-page)] :block/tags :logseq.class/Page]]]
             {:tx-meta tx-meta
              :tx-data tx-data})))

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff