Просмотр исходного кода

Merge branch 'refactor/db-properties-schema' into refactor/db-remove-block-name-unique

Tienson Qin 1 год назад
Родитель
Сommit
8d128f420c
38 измененных файлов с 691 добавлено и 584 удалено
  1. 7 3
      deps/db/src/logseq/db/frontend/property.cljs
  2. 12 5
      deps/db/src/logseq/db/frontend/property/type.cljs
  3. 25 19
      deps/db/src/logseq/db/frontend/property/util.cljs
  4. 28 17
      deps/db/src/logseq/db/sqlite/create_graph.cljs
  5. 3 4
      deps/db/src/logseq/db/sqlite/util.cljs
  6. 1 1
      deps/shui/src/logseq/shui/popup/core.cljs
  7. 2 0
      libs/src/LSPlugin.ts
  8. 1 1
      libs/src/modules/LSPlugin.Experiments.ts
  9. 8 4
      scripts/src/logseq/tasks/db_graph/create_graph.cljs
  10. 10 12
      scripts/src/logseq/tasks/db_graph/create_graph_with_properties.cljs
  11. 1 1
      scripts/src/logseq/tasks/dev.clj
  12. 0 1
      shadow-cljs.edn
  13. 15 12
      src/main/frontend/components/block.cljs
  14. 1 1
      src/main/frontend/components/page.css
  15. 1 1
      src/main/frontend/components/property.cljs
  16. 7 5
      src/main/frontend/components/property/closed_value.cljs
  17. 3 3
      src/main/frontend/components/property/value.cljs
  18. 56 53
      src/main/frontend/components/right_sidebar.cljs
  19. 0 3
      src/main/frontend/config.cljs
  20. 5 2
      src/main/frontend/db_worker.cljs
  21. 83 47
      src/main/frontend/handler/db_based/property.cljs
  22. 4 1
      src/main/frontend/handler/editor.cljs
  23. 13 22
      src/main/frontend/handler/history.cljs
  24. 1 1
      src/main/frontend/handler/property/util.cljs
  25. 0 211
      src/main/frontend/modules/editor/undo_redo.cljs
  26. 3 16
      src/main/frontend/modules/outliner/pipeline.cljs
  27. 0 6
      src/main/frontend/modules/shortcut/config.cljs
  28. 0 1
      src/main/frontend/state.cljs
  29. 0 8
      src/main/frontend/util/page.cljs
  30. 3 10
      src/main/frontend/worker/batch_tx.clj
  31. 15 14
      src/main/frontend/worker/pipeline.cljs
  32. 132 83
      src/main/frontend/worker/undo_redo.cljs
  33. 5 0
      src/main/logseq/api.cljs
  34. 26 5
      src/main/logseq/sdk/experiments.cljs
  35. 0 1
      src/resources/dicts/en.edn
  36. 27 3
      src/test/frontend/handler/db_based/property_test.cljs
  37. 30 0
      src/test/frontend/test/generators.cljs
  38. 163 7
      src/test/frontend/worker/undo_redo_test.cljs

+ 7 - 3
deps/db/src/logseq/db/frontend/property.cljs

@@ -277,12 +277,16 @@
                 e))) values)))
 
 ;; TODO: db ident should obey clojure's rules for keywords
-(defn user-property-ident-from-name
-  "Makes a user property :db/ident from a name by sanitizing the given name"
+(defn create-user-property-ident-from-name
+  "Creates a user property :db/ident from a name by sanitizing the given name.
+
+   NOTE: Only use this when creating a db-ident for a property name. Using this
+   in read-only contexts like querying can result in db-ident conflicts"
   [property-name]
   (let [n (-> (string/lower-case property-name)
-              (string/replace #"^:\s*" "")
+              (string/replace #"(^:\s*|\s*:$)" "")
               (string/replace #"\s*:\s*$" "")
+              (string/replace-first #"^\d+" "")
               (string/replace " " "-")
               (string/replace "#" "")
               (string/trim))]

+ 12 - 5
deps/db/src/logseq/db/frontend/property/type.cljs

@@ -12,7 +12,7 @@
 
 (def internal-built-in-property-types
   "Valid property types only for use by internal built-in-properties"
-  #{:keyword :map :coll :any :uuid :entity})
+  #{:keyword :map :coll :any :entity})
 
 (def user-built-in-property-types
   "Valid property types for users in order they appear in the UI"
@@ -25,6 +25,13 @@
 (assert (set/subset? closed-value-property-types (set user-built-in-property-types))
         "All closed value types are valid property types")
 
+(def ref-property-types #{:page :date :entity})
+
+(assert (set/subset? ref-property-types
+                     (into internal-built-in-property-types
+                           user-built-in-property-types))
+        "All ref types are valid property types")
+
 (def ^:private user-built-in-allowed-schema-attributes
   "Map of types to their set of allowed :schema attributes"
   (merge-with into
@@ -70,11 +77,12 @@
   "Validates that the given existing closed value is valid"
   [db property type-validate-fn value]
   (boolean
-   (when-let [e (and (uuid? value)
-                     (d/entity db [:block/uuid value]))]
+   (when-let [e (if (uuid? value)
+                  (d/entity db [:block/uuid value])
+                  (d/entity db value))]
      (let [values (get-in property [:block/schema :values])]
        (and
-        (contains? (set values) value)
+        (contains? (set values) (:block/uuid e))
         (if (contains? (:block/type e) "closed value")
           (type-validate-fn (:value (:block/schema e)))
           ;; page uuids aren't closed value types
@@ -119,7 +127,6 @@
               entity?]
    ;; internal usage
    :keyword  keyword?
-   :uuid     uuid?
    :map      map?
    ;; coll elements are ordered as it's saved as a vec
    :coll     coll?

+ 25 - 19
deps/db/src/logseq/db/frontend/property/util.cljs

@@ -2,7 +2,7 @@
   "Util fns for building core property concepts"
   (:require [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.frontend.default :as default-db]
-            [datascript.core :as d]))
+            [logseq.db.frontend.property.type :as db-property-type]))
 
 (defonce hidden-page-name-prefix "$$$")
 
@@ -19,8 +19,9 @@
 (defn build-closed-value-block
   "Builds a closed value block to be transacted"
   [block-uuid block-value page-id property {:keys [db-ident icon description]}]
+  (assert block-uuid (uuid? block-uuid))
   (cond->
-   (closed-value-new-block page-id (or block-uuid (d/squuid)) block-value property)
+   (closed-value-new-block page-id block-uuid block-value property)
     (and db-ident (keyword? db-ident))
     (assoc :db/ident db-ident)
 
@@ -49,21 +50,26 @@
    the hidden page and closed value blocks as needed"
   [db-ident prop-name property {:keys [translate-closed-page-value-fn property-attributes]
                                 :or {translate-closed-page-value-fn identity}}]
-  (let [page-tx (build-property-hidden-page property)
-        page-id [:block/uuid (:block/uuid page-tx)]
-        closed-value-page-uuids? (contains? #{:page :date} (get-in property [:block/schema :type]))
-        closed-value-blocks-tx
-        (if closed-value-page-uuids?
-          (map translate-closed-page-value-fn (:closed-values property))
-          (map (fn [{:keys [db-ident value icon description uuid]}]
-                 (build-closed-value-block
-                  uuid value page-id property {:db-ident db-ident
-                                               :icon icon
-                                               :description description}))
-               (:closed-values property)))
-        property-schema (assoc (:block/schema property)
-                               :values (mapv :block/uuid closed-value-blocks-tx))
+  (let [property-schema (assoc (:block/schema property)
+                               :values
+                               (if (contains? db-property-type/ref-property-types (get-in property [:block/schema :type]))
+                                 (mapv translate-closed-page-value-fn (:closed-values property))
+                                 (mapv :uuid (:closed-values property))))
         property-tx (merge (sqlite-util/build-new-property db-ident property-schema {:original-name prop-name})
-                           property-attributes)]
-    (into [property-tx page-tx]
-          (when-not closed-value-page-uuids? closed-value-blocks-tx))))
+                           property-attributes)
+        hidden-tx
+        ;; closed ref types don't have hidden tx
+        (if (contains? db-property-type/ref-property-types (get-in property [:block/schema :type]))
+          []
+          (let [page-tx (build-property-hidden-page property)
+                closed-value-blocks-tx
+                (map (fn [{:keys [db-ident value icon description uuid]}]
+                       (build-closed-value-block
+                        uuid
+                        value
+                        [:block/uuid (:block/uuid page-tx)]
+                        property
+                        {:db-ident db-ident :icon icon :description description}))
+                     (:closed-values property))]
+            (into [page-tx] closed-value-blocks-tx)))]
+    (into [property-tx] hidden-tx)))

+ 28 - 17
deps/db/src/logseq/db/sqlite/create_graph.cljs

@@ -45,6 +45,8 @@
   #{"Contents"})
 
 (defn build-db-initial-data
+  "Builds tx of initial data for a new graph including key values, initial files,
+   built-in properties and built-in classes"
   [config-content]
   (let [initial-data [(kv :db/type "db")
                       (kv :schema/version db-schema/version)
@@ -72,20 +74,29 @@
                          (fn [[db-ident {:keys [schema original-name]}]]
                            (let [original-name' (or original-name (name db-ident))]
                              (default-db/mark-block-as-built-in
-                             (sqlite-util/build-new-class
-                              (let [properties (mapv
-                                                (fn [db-ident]
-                                                  (let [property (get db-ident->properties db-ident)]
-                                                    (assert property (str "Built-in property " db-ident " is not defined yet"))
-                                                    db-ident))
-                                                (:properties schema))]
-                                (cond->
-                                 {:block/original-name original-name'
-                                  :block/name (common-util/page-name-sanity-lc original-name')
-                                  :db/ident db-ident
-                                  :block/uuid (d/squuid)}
-                                  (seq properties)
-                                  (assoc :class/schema.properties properties)))))))
-                         db-class/built-in-classes)]
-    (vec (concat default-properties default-classes
-                 initial-data initial-files default-pages))))
+                              (sqlite-util/build-new-class
+                               (let [properties (mapv
+                                                 (fn [db-ident]
+                                                   (let [property (get db-ident->properties db-ident)]
+                                                     (assert property (str "Built-in property " db-ident " is not defined yet"))
+                                                     db-ident))
+                                                 (:properties schema))]
+                                 (cond->
+                                  {:block/original-name original-name'
+                                   :block/name (common-util/page-name-sanity-lc original-name')
+                                   :db/ident db-ident
+                                   :block/uuid (d/squuid)}
+                                   (seq properties)
+                                   (assoc :class/schema.properties properties)))))))
+                         db-class/built-in-classes)
+        tx (vec (concat default-properties default-classes
+                        initial-data initial-files default-pages))]
+    (when-let [conflicting-idents
+               (->> (keep :db/ident tx)
+                    frequencies
+                    (keep (fn [[k v]] (when (> v 1) k)))
+                    seq)]
+      (throw (ex-info (str "The following :db/idents are not unique and clobbered each other: "
+                           (vec conflicting-idents))
+                      {:idents conflicting-idents})))
+    tx))

+ 3 - 4
deps/db/src/logseq/db/sqlite/util.cljs

@@ -8,6 +8,7 @@
             [datascript.impl.entity :as de]
             [datascript.core :as d]
             [cljs-bean.transit]
+            [logseq.db.frontend.property.type :as db-property-type]
             [logseq.db.frontend.property :as db-property]))
 
 (defonce db-version-prefix "logseq_db_")
@@ -65,8 +66,6 @@
                 (assoc :block/created-at updated-at))]
     block))
 
-(def property-ref-types #{:page :block :date :entity})
-
 (defn build-new-property
   "Build a standard new property so that it is is consistent across contexts. Takes
    an optional map with following keys:
@@ -76,7 +75,7 @@
    (assert (keyword? db-ident))
    (let [db-ident' (if (qualified-keyword? db-ident)
                      db-ident
-                     (db-property/user-property-ident-from-name (name db-ident)))
+                     (db-property/create-user-property-ident-from-name (name db-ident)))
          prop-name (or original-name (name db-ident'))]
      (block-with-timestamps
       (cond->
@@ -92,7 +91,7 @@
         :db/cardinality (if (= :many (:cardinality prop-schema))
                           :db.cardinality/many
                           :db.cardinality/one)}
-        (contains? property-ref-types (:type prop-schema))
+        (contains? db-property-type/ref-property-types (:type prop-schema))
         (assoc :db/valueType :db.type/ref))))))
 
 

+ 1 - 1
deps/shui/src/logseq/shui/popup/core.cljs

@@ -196,4 +196,4 @@
     [:<>
      (for [config popups
            :when (and (map? config) (:id config))]
-       (x-popup config))]))
+       (rum/with-key (x-popup config) (:id config)))]))

+ 2 - 0
libs/src/LSPlugin.ts

@@ -623,6 +623,8 @@ export interface IEditorProxy extends Record<string, any> {
 
   getSelectedBlocks: () => Promise<Array<BlockEntity> | null>
 
+  clearSelectedBlocks: () => Promise<void>
+
   /**
    * get all blocks of the current page as a tree structure
    *

+ 1 - 1
libs/src/modules/LSPlugin.Experiments.ts

@@ -21,7 +21,7 @@ export class LSPluginExperiments {
   get Components() {
     const exper = this.ensureHostScope().logseq.sdk.experiments
     return {
-      Editor: exper.cp_page_editor as (props: { page: string }) => any
+      Editor: exper.cp_page_editor as (props: { page: string } & any) => any
     }
   }
 

+ 8 - 4
scripts/src/logseq/tasks/db_graph/create_graph.cljs

@@ -57,6 +57,10 @@
       (if-let [page-uuid (page-uuids (second val))]
         [:block/uuid page-uuid]
         (throw (ex-info (str "No uuid for page '" (second val) "'") {:name (second val)})))
+      :page-uuid
+      (if-let [page-uuid (page-uuids (second val))]
+        page-uuid
+        (throw (ex-info (str "No uuid for page '" (second val) "'") {:name (second val)})))
       :block
       (or (block-uuids (second val))
           (throw (ex-info (str "No uuid for block '" (second val) "'") {:name (second val)})))
@@ -68,7 +72,7 @@
   (->> properties
        (map
         (fn [[prop-name val]]
-          [(db-property/user-property-ident-from-name (name prop-name))
+          [(db-property/create-user-property-ident-from-name (name prop-name))
             ;; set indicates a :many value
            (if (set? val)
              (set (map #(translate-property-value % uuid-maps) val))
@@ -105,7 +109,7 @@
 (defn- build-property-refs [properties property-db-ids]
   (mapv
    (fn [prop-name]
-     (db-property/user-property-ident-from-name (name prop-name)))
+     (db-property/create-user-property-ident-from-name (name prop-name)))
    (keys properties)))
 
 (def current-db-id (atom 0))
@@ -168,13 +172,13 @@
                            (mapcat
                             (fn [[prop-name]]
                               (if (get-in properties [prop-name :closed-values])
-                                (let [db-ident (db-property/user-property-ident-from-name (name prop-name))]
+                                (let [db-ident (db-property/create-user-property-ident-from-name (name prop-name))]
                                   (db-property-util/build-closed-values
                                    db-ident
                                    prop-name
                                    (assoc (get properties prop-name) :db/ident db-ident)
                                    {:translate-closed-page-value-fn
-                                    #(hash-map :block/uuid (translate-property-value (:value %) uuid-maps))
+                                    #(translate-property-value (:value %) uuid-maps)
                                     :property-attributes
                                     {:db/id (or (property-db-ids (name prop-name))
                                                 (throw (ex-info "No :db/id for property" {:property prop-name})))}}))

+ 10 - 12
scripts/src/logseq/tasks/db_graph/create_graph_with_properties.cljs

@@ -40,10 +40,10 @@
                     :uuid (random-uuid))
          [10 42 (rand 100)])
    :page-closed
-   (mapv #(hash-map :value [:page %])
+   (mapv #(hash-map :value [:page-uuid %])
          ["page 1" "page 2" "page 3"])
    :date-closed
-   (mapv #(hash-map :value [:page (date-journal-title %)])
+   (mapv #(hash-map :value [:page-uuid (date-journal-title %)])
          dates)})
 
 (defn- create-init-data
@@ -59,9 +59,8 @@
                                (:uuid val))
         random-page-closed-value #(let [val (-> closed-values-config % rand-nth :value)]
                                     (swap! closed-values assoc % (second val))
-                                    val)
-        ;; TODO: Remove default when page-closed/date-closed re-enabled
-        get-closed-value #(get @closed-values % "")]
+                                    [:page (second val)])
+        get-closed-value #(get @closed-values %)]
     {:pages-and-blocks
      ;; Journals
      [{:page
@@ -89,12 +88,11 @@
         {:block/content "number-closed property block" :properties {:number-closed (random-closed-value :number-closed)}}
         {:block/content "page property block" :properties {:page [:page "page 1"]}}
         {:block/content "page-many property block" :properties {:page-many #{[:page "page 1"] [:page "page 2"]}}}
-        ;; TODO: Fix page-closed and date-closed
-        #_{:block/content "page-closed property block" :properties {:page-closed (random-page-closed-value :page-closed)}}
+        {:block/content "page-closed property block" :properties {:page-closed (random-page-closed-value :page-closed)}}
         {:block/content "date property block" :properties {:date [:page (date-journal-title today)]}}
         {:block/content "date-many property block" :properties {:date-many #{[:page (date-journal-title today)]
                                                                              [:page (date-journal-title yesterday)]}}}
-        #_{:block/content "date-closed property block" :properties {:date-closed (random-page-closed-value :date-closed)}}]}
+        {:block/content "date-closed property block" :properties {:date-closed (random-page-closed-value :date-closed)}}]}
       {:page {:block/original-name "Block Property Queries"}
        :blocks
        [{:block/content "{{query (property :default \"haha\")}}"}
@@ -111,7 +109,7 @@
         {:block/content (str "{{query (property :page-closed " (page-ref/->page-ref (string/capitalize (get-closed-value :page-closed))) ")}}")}
         {:block/content (str "{{query (property :date " (page-ref/->page-ref (string/capitalize (date-journal-title today))) ")}}")}
         {:block/content (str "{{query (property :date-many " (page-ref/->page-ref (string/capitalize (date-journal-title yesterday))) ")}}")}
-        #_{:block/content (str "{{query (property :date-closed " (page-ref/->page-ref (string/capitalize (get-closed-value :date-closed))) ")}}")}]}
+        {:block/content (str "{{query (property :date-closed " (page-ref/->page-ref (string/capitalize (get-closed-value :date-closed))) ")}}")}]}
 
       ;; Property values
       ;; Needs to be before page property pages b/c they are referenced by them
@@ -134,11 +132,11 @@
       {:page {:block/name "number-closed page" :properties {:number-closed (random-closed-value :number-closed)}}}
       {:page {:block/name "page page" :properties {:page [:page "page 1"]}}}
       {:page {:block/name "page-many page" :properties {:page-many #{[:page "page 1"] [:page "page 2"]}}}}
-      #_{:page {:block/name "page-closed page" :properties {:page-closed (random-page-closed-value :page-closed)}}}
+      {:page {:block/name "page-closed page" :properties {:page-closed (random-page-closed-value :page-closed)}}}
       {:page {:block/name "date page" :properties {:date [:page (date-journal-title today)]}}}
       {:page {:block/name "date-many page" :properties {:date-many #{[:page (date-journal-title today)]
                                                                      [:page (date-journal-title yesterday)]}}}}
-      #_{:page {:block/name "date-closed page" :properties {:date-closed (random-page-closed-value :date-closed)}}}
+      {:page {:block/name "date-closed page" :properties {:date-closed (random-page-closed-value :date-closed)}}}
       {:page {:block/original-name "Page Property Queries"}
        :blocks
        [{:block/content "{{query (page-property :default \"yolo\")}}"}
@@ -166,7 +164,7 @@
           (into (mapv #(vector (keyword (str (name %) "-closed"))
                                {:closed-values (closed-values-config (keyword (str (name %) "-closed")))
                                 :block/schema {:type %}})
-                      [:default :url :number #_:page #_:date]))
+                      [:default :url :number :page :date]))
           (into {}))}))
 
 (def spec

+ 1 - 1
scripts/src/logseq/tasks/dev.clj

@@ -34,7 +34,7 @@
   (apply shell "yarn cljs:run-test" args))
 
 (defn lint-and-test
-  "Run all lint tasks, then run tests(exclude testcases taged by :long).
+  "Run all lint tasks, then run tests(exclude testcases tagged by :long).
   pass args through to cmd 'yarn cljs:run-test'"
   []
   (lint)

+ 0 - 1
shadow-cljs.edn

@@ -52,7 +52,6 @@
                           frontend.config/ENABLE-PLUGINS #shadow/env ["ENABLE_PLUGINS" :as :bool :default true]
                           ;; Set to switch file sync server to dev, set this to false in `yarn watch`
                           frontend.config/ENABLE-FILE-SYNC-PRODUCTION #shadow/env ["ENABLE_FILE_SYNC_PRODUCTION" :as :bool :default true]
-                          frontend.config/TEST #shadow/env ["LOGSEQ_CI" :as :bool :default false]
                           frontend.config/REVISION #shadow/env ["LOGSEQ_REVISION" :default "dev"]} ;; set by git-revision-hook
 
         ;; NOTE: electron, browser/mobile-app use different asset-paths.

+ 15 - 12
src/main/frontend/components/block.cljs

@@ -910,9 +910,7 @@
           (let [title [:span.block-ref
                        (block-content (assoc config :block-ref? true :stop-events? stop-inner-events?)
                                       block nil (:block/uuid block)
-                                      (:slide? config)
-                                      false
-                                      (atom nil))]
+                                      (:slide? config))]
                 inner (if label
                         (->elem
                          :span.block-ref
@@ -1808,11 +1806,14 @@
                        (state/toggle-collapsed-block! uuid)
                        (if collapsed?
                          (editor-handler/expand-block! uuid)
-                         (editor-handler/collapse-block! uuid))))}
+                         (editor-handler/collapse-block! uuid)))
+                     ;; debug config context
+                     (when (and (state/developer-mode?) (.-metaKey event))
+                       (js/console.debug "[block config]==" config)))}
         [:span {:class (if (or (and control-show?
-                                    (or collapsed?
-                                        (editor-handler/collapsable? uuid {:semantic? true})))
-                               (and collapsed? (or order-list? config/publishing?)))
+                                 (or collapsed?
+                                   (editor-handler/collapsable? uuid {:semantic? true})))
+                             (and collapsed? (or order-list? config/publishing?)))
                          "control-show cursor-pointer"
                          "control-hide")}
          (ui/rotating-arrow collapsed?)]])
@@ -2505,7 +2506,7 @@
              (assoc state
                     ::hide-block-refs? (atom default-hide?)
                     ::refs-count *refs-count)))}
-  [state config {:block/keys [uuid format] :as block} {:keys [edit-input-id block-id edit? hide-block-refs-count? selected?]}]
+  [state config {:block/keys [uuid format] :as block} {:keys [edit-input-id block-id edit? hide-block-refs-count?]}]
   (let [*hide-block-refs? (get state ::hide-block-refs?)
         *refs-count (get state ::refs-count)
         hide-block-refs? (rum/react *hide-block-refs?)
@@ -2536,7 +2537,9 @@
                                                    (p/do!
                                                     (state/set-editor-op! :escape)
                                                     (editor-handler/save-block! (editor-handler/get-state) value)
-                                                    (editor-handler/escape-editing select?))))}
+                                                    (editor-handler/escape-editing select?)
+                                                    (some-> config :on-escape-editing
+                                                            (apply [(str uuid) (= event :esc)])))))}
                                      edit-input-id
                                      config))]
           [:div.flex.flex-1.flex-row.gap-1.items-start
@@ -2550,15 +2553,15 @@
           [:div.flex.flex-row
            [:div.flex-1.w-full {:style {:display (if (:slide? config) "block" "flex")}}
             (ui/catch-error
-             (ui/block-error "Block Render Error:"
-                             {:content (:block/content block)
+              (ui/block-error "Block Render Error:"
+                {:content (:block/content block)
                               :section-attrs
                               {:on-click #(let [content (or (:block/original-name block)
                                                             (:block/content block))]
                                             (editor-handler/clear-selection!)
                                             (editor-handler/unhighlight-blocks!)
                                             (state/set-editing! edit-input-id content block "" {:container-id (:container-id config)}))}})
-             (block-content config block edit-input-id block-id slide? selected?))]
+              (block-content config block edit-input-id block-id slide?))]
 
            (when (and (not hide-block-refs-count?)
                       (not named?))

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

@@ -402,7 +402,7 @@ html.is-native-ios {
   }
 
   .ls-new-property {
-    @apply mt-1;
+    @apply mt-[3px];
   }
 
   &.is-collapsed {

+ 1 - 1
src/main/frontend/components/property.cljs

@@ -170,7 +170,7 @@
                   (state/set-state! :editor/editing-property-value-id
                                     {id true}))
                 (property-handler/set-block-property! repo (:block/uuid block)
-                                                      property-name
+                                                      (:db/ident (db/entity [:block/original-name property-name]))
                                                       (if (= type :default) "" :logseq.property/empty-placeholder)))))))}
       (shui/select-trigger
        {:class "!px-2 !py-0 !h-8"}

+ 7 - 5
src/main/frontend/components/property/closed_value.cljs

@@ -17,7 +17,8 @@
             [frontend.db.async :as db-async]
             [frontend.state :as state]
             [promesa.core :as p]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.property.type :as db-property-type]))
 
 (defn- <upsert-closed-value!
   "Create new closed value and returns its block UUID."
@@ -127,8 +128,7 @@
                                      {:on-change (fn [page]
                                                    (db-property-handler/replace-closed-value property
                                                                                              (:db/id page)
-                                                                                             (:db/id item)))})
-         ((:page-cp parent-opts) {:preview? false} item)]
+                                                                                             (:db/id item)))})]
 
         (and page? (:page-cp parent-opts))
         ((:page-cp parent-opts) {:preview? false} item)
@@ -184,7 +184,7 @@
    [:ol
     (for [value values]
       [:li (if (uuid? value)
-             (let [result (db/entity value)]
+             (let [result (db/entity [:block/uuid value])]
                (:block/original-name result))
              (str value))])]
    (ui/button
@@ -225,7 +225,9 @@
              (shui/popup-show! (.-target e)
                                (fn [{:keys [id]}]
                                  (let [opts {:toggle-fn (fn [] (shui/popup-hide! id))}
-                                       values' (->> (map second values)
+                                       values' (->> (if (contains? db-property-type/ref-property-types (get-in property [:block/schema :type]))
+                                                      (map #(:block/uuid (db/entity (second %))) values)
+                                                      (map second values))
                                                     (remove string/blank?)
                                                     (remove (set (get-in property [:block/schema :values])))
                                                     distinct)]

+ 3 - 3
src/main/frontend/components/property/value.cljs

@@ -619,10 +619,10 @@
        :onEscapeKeyDown #(set-open! false)}
       [:div.property-select
        (case type
-         (:number :url :date :default)
+         (:number :url :default)
          (select block property select-opts' opts)
 
-         :page
+         (:page :date)
          (property-value-select-page block property select-opts' opts))]))))
 
 (defn- property-editing
@@ -756,7 +756,7 @@
                     (if (seq items)
                       (concat
                        (for [item items]
-                         (select-item property type item opts))
+                         (rum/with-key (select-item property type item opts) (or (:block/uuid item) (str item))))
                        (when date?
                          [(property-value-date-picker block property nil {:toggle-fn toggle-fn})]))
                       (when-not editing?

+ 56 - 53
src/main/frontend/components/right_sidebar.cljs

@@ -17,8 +17,6 @@
             [frontend.ui :as ui]
             [logseq.shui.ui :as shui]
             [frontend.util :as util]
-            [frontend.config :as config]
-            [frontend.modules.editor.undo-redo :as undo-redo]
             [medley.core :as medley]
             [reitit.frontend.easy :as rfe]
             [rum.core :as rum]
@@ -68,49 +66,53 @@
                          :sidebar-key sidebar-key} repo block-id {:indent? false})]
      (block-cp repo idx block)]))
 
-(rum/defc history-action-info
-  [[k v]]
-  (when v [:.ml-4 (ui/foldable
-                   [:div (str k)]
-                   [:.ml-4 (case k
-                             :tx-id
-                             [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
-
-                             :blocks
-                             (map (fn [block]
-                                    [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
-
-                             :txs
-                             (map (fn [[_ key val]]
-                                    (when val
-                                      [:pre.code.pre-wrap-white-space.bg-base-4
-                                       [:span.font-bold (str key) " "] (str val)])) v)
-
-                             (map (fn [[key val]]
-                                    (when val
-                                      [:pre.code.pre-wrap-white-space.bg-base-4
-                                       [:span.font-bold (str key) " "] (str val)])) v))]
-                   {:default-collapsed? true})]))
-
-(rum/defc history-stack
-  [label stack]
-  [:.ml-4 (ui/foldable
-           [:div label " (" (count stack) ")"]
-           (map-indexed (fn [index item]
-                          [:.ml-4 (ui/foldable [:div (str index " " (-> item :tx-meta :outliner-op))]
-                                               (map history-action-info item)
-                                               {:default-collapsed? true})]) stack)
-           {:default-collapsed? true})])
-
-(rum/defc history < rum/reactive
-  []
-  (let [state (undo-redo/get-state)
-        page-only-mode? (state/sub :history/page-only-mode?)]
-    [:div.ml-4
-     [:div.ml-3.font-bold (if page-only-mode? (t :right-side-bar/history-pageonly) (t :right-side-bar/history-global))]
-     [:div.p-4 [:.ml-4.mb-2
-                (history-stack (t :right-side-bar/history-undos) (rum/react (:undo-stack state)))
-                (history-stack (t :right-side-bar/history-redos) (rum/react (:redo-stack state)))]]]))
+(comment
+  (rum/defc history-action-info
+   [[k v]]
+   (when v [:.ml-4 (ui/foldable
+                    [:div (str k)]
+                    [:.ml-4 (case k
+                              :tx-id
+                              [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str v)]]
+
+                              :blocks
+                              (map (fn [block]
+                                     [:.my-1 [:pre.code.pre-wrap-white-space.bg-base-4 (str block)]]) v)
+
+                              :txs
+                              (map (fn [[_ key val]]
+                                     (when val
+                                       [:pre.code.pre-wrap-white-space.bg-base-4
+                                        [:span.font-bold (str key) " "] (str val)])) v)
+
+                              (map (fn [[key val]]
+                                     (when val
+                                       [:pre.code.pre-wrap-white-space.bg-base-4
+                                        [:span.font-bold (str key) " "] (str val)])) v))]
+                    {:default-collapsed? true})])))
+
+(comment
+  (rum/defc history-stack
+   [label stack]
+   [:.ml-4 (ui/foldable
+            [:div label " (" (count stack) ")"]
+            (map-indexed (fn [index item]
+                           [:.ml-4 (ui/foldable [:div (str index " " (-> item :tx-meta :outliner-op))]
+                                                (map history-action-info item)
+                                                {:default-collapsed? true})]) stack)
+            {:default-collapsed? true})]))
+
+(comment
+  (rum/defc history < rum/reactive
+   []
+  ;; (let [state (undo-redo/get-state)
+  ;;       page-only-mode? (state/sub :history/page-only-mode?)]
+  ;;   [:div.ml-4
+  ;;    [:div.ml-3.font-bold (if page-only-mode? (t :right-side-bar/history-pageonly) (t :right-side-bar/history-global))]
+  ;;    [:div.p-4 [:.ml-4.mb-2
+  ;;               (history-stack (t :right-side-bar/history-undos) (rum/react (:undo-stack state)))
+  ;;               (history-stack (t :right-side-bar/history-redos) (rum/react (:redo-stack state)))]]])
+   ))
 
 (defn build-sidebar-item
   [repo idx db-id block-type *db-id init-key]
@@ -126,9 +128,9 @@
     [[:.flex.items-center (ui/icon "hierarchy" {:class "text-md mr-2"}) (t :right-side-bar/page-graph)]
      (page/page-graph)]
 
-    :history
-    [[:.flex.items-center (ui/icon "history" {:class "text-md mr-2"}) (t :right-side-bar/history)]
-     (history)]
+    ;; :history
+    ;; [[:.flex.items-center (ui/icon "history" {:class "text-md mr-2"}) (t :right-side-bar/history)]
+    ;;  (history)]
 
     :block-ref
     #_:clj-kondo/ignore
@@ -457,11 +459,12 @@
                                                                        (state/sidebar-add-block! repo "rtc" :rtc))}
             "(Dev) RTC"]])
 
-        (when (and config/dev? (state/sub [:ui/developer-mode?]))
-          [:div.text-sm
-           [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
-                                                                       (state/sidebar-add-block! repo "history" :history))}
-            (t :right-side-bar/history)]])]]
+        ;; (when (and config/dev? (state/sub [:ui/developer-mode?]))
+        ;;   [:div.text-sm
+        ;;    [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
+        ;;                                                                (state/sidebar-add-block! repo "history" :history))}
+        ;;     (t :right-side-bar/history)]])
+        ]]
 
       [:.sidebar-item-list.flex-1.scrollbar-spacing.px-2
        (if @*anim-finished?

+ 0 - 3
src/main/frontend/config.cljs

@@ -25,9 +25,6 @@
 
 (reset! state/publishing? publishing?)
 
-(goog-define TEST false)
-(def test? TEST)
-
 (def ENABLE-FILE-SYNC-PRODUCTION false)
 
 ;; this is a feature flag to enable the account tab

+ 5 - 2
src/main/frontend/db_worker.cljs

@@ -693,11 +693,14 @@
 
   (undo
    [_this repo]
-   (undo-redo/undo repo)
+   (when-let [conn (worker-state/get-datascript-conn repo)]
+     (undo-redo/undo repo conn))
    nil)
+
   (redo
    [_this repo]
-   (undo-redo/redo repo))
+   (when-let [conn (worker-state/get-datascript-conn repo)]
+     (undo-redo/redo repo conn)))
 
   (keep-alive
    [_this]

+ 83 - 47
src/main/frontend/handler/db_based/property.cljs

@@ -16,6 +16,7 @@
             [malli.util :as mu]
             [malli.error :as me]
             [logseq.common.util.page-ref :as page-ref]
+            [datascript.core :as d]
             [datascript.impl.entity :as e]
             [logseq.db.frontend.property :as db-property]
             [frontend.handler.property.util :as pu]
@@ -95,7 +96,7 @@
   [property {:keys [type cardinality]}]
   (let [ident (:db/ident property)
         cardinality (if (= cardinality :many) :db.cardinality/many :db.cardinality/one)
-        type-data (when (and type (sqlite-util/property-ref-types type)) ; type changes
+        type-data (when (and type (db-property-type/ref-property-types type)) ; type changes
                     {:db/ident ident
                      :db/valueType :db.type/ref
                      :db/cardinality cardinality})]
@@ -103,14 +104,36 @@
         {:db/ident ident
          :db/cardinality cardinality})))
 
+(defn- ensure-unique-db-ident
+  "Ensures the given db-ident is unique. If a db-ident conflicts, it is made
+  unique by adding a suffix with a unique number e.g. :db-ident-1 :db-ident-2"
+  [db db-ident]
+  (if (d/entity db db-ident)
+    (let [existing-idents
+          (d/q '[:find [?ident ...]
+                 :in $ ?ident-name
+                 :where
+                 [?b :db/ident ?ident]
+                 [(str ?ident) ?str-ident]
+                 [(clojure.string/starts-with? ?str-ident ?ident-name)]]
+               db
+               (str db-ident "-"))
+          new-ident (if-let [max-num (->> existing-idents
+                                          (keep #(parse-long (string/replace-first (str %) (str db-ident "-") "")))
+                                          (apply max))]
+                      (keyword (namespace db-ident) (str (name db-ident) "-" (inc max-num)))
+                      (keyword (namespace db-ident) (str (name db-ident) "-1")))]
+      new-ident)
+    db-ident))
+
 (defn upsert-property!
-  "Updates property for property-id if it exists or creates a new property.
-   Two main  ways to create a property are to set property-id to a qualified keyword
-   or to set it to nil and pass :property-name as an option"
+  "Updates property if property-id is given. Otherwise creates a property
+   with the given property-id or :property-name option. When a property is created
+   it is ensured to have a unique :db/ident"
   [repo property-id schema {:keys [property-name properties]}]
-  (let [db-ident (or property-id (db-property/user-property-ident-from-name property-name))]
+  (let [db-ident (or property-id (db-property/create-user-property-ident-from-name property-name))]
     (assert (qualified-keyword? db-ident))
-    (if-let [property (db/entity db-ident)]
+    (if-let [property (and (qualified-keyword? property-id) (db/entity db-ident))]
       (let [tx-data (->>
                      (conj
                       [(cond->
@@ -129,10 +152,12 @@
                                     :property-id (:db/id property)
                                     :many->one? many->one?}))
       (let [k-name (or (and property-name (name property-name))
-                       (name property-id))]
+                       (name property-id))
+            db-ident' (ensure-unique-db-ident (db/get-db repo) db-ident)]
         (assert (some? k-name)
                 (prn "property-id: " property-id ", property-name: " property-name))
-        (db/transact! repo [(sqlite-util/build-new-property db-ident schema {:original-name k-name})]
+        (db/transact! repo
+                      [(sqlite-util/build-new-property db-ident' schema {:original-name k-name})]
                       {:outliner-op :new-property})))))
 
 (defn validate-property-value
@@ -202,9 +227,6 @@
 (defn set-block-property!
   [repo block-eid property-id v {:keys [property-name] :as opts}]
   (let [block-eid (->eid block-eid)
-        property-id (if (string? property-id)
-                      (db-property/user-property-ident-from-name property-id)
-                      property-id)
         _ (assert (keyword? property-id) "property-id should be a keyword")
         block (db/entity repo block-eid)
         property (db/entity property-id)
@@ -267,19 +289,26 @@
   [repo class-uuid property-id]
   (when-let [class (db/entity repo [:block/uuid class-uuid])]
     (when (contains? (:block/type class) "class")
-      (let [[db-ident property]
+      (let [[db-ident property options]
             ;; strings come from user
             (if (string? property-id)
               (if-let [ent (db/entity [:block/original-name property-id])]
-                [(:db/ident ent) ent]
-                [(db-property/user-property-ident-from-name property-id) nil])
-              [property-id (db/entity property-id)])
+                [(:db/ident ent) ent {}]
+                ;; creates ident beforehand b/c needed in later transact and this avoids
+                ;; making this whole fn async for now
+                [(ensure-unique-db-ident
+                  (db/get-db (state/get-current-repo))
+                  (db-property/create-user-property-ident-from-name property-id))
+                 nil
+                 {:property-name property-id}])
+              [property-id (db/entity property-id) {}])
             property-type (get-in property [:block/schema :type])
-            _ (upsert-property! repo db-ident
+            _ (upsert-property! repo
+                                db-ident
                                 (cond-> (:block/schema property)
                                   (some? property-type)
                                   (assoc :type property-type))
-                                {})]
+                                options)]
         (db/transact! repo
                       [[:db/add (:db/id class) :class/schema.properties db-ident]]
                       {:outliner-op :save-block})))))
@@ -291,7 +320,7 @@
       (when-let [property (db/entity repo property-id)]
         (when-not (ldb/built-in-class-property? class property)
           (db/transact! repo [[:db/retract (:db/id class) :class/schema.properties property-id]]
-            {:outliner-op :save-block}))))))
+                        {:outliner-op :save-block}))))))
 
 (defn class-set-schema!
   [repo class-uuid schema]
@@ -339,24 +368,23 @@
       (let [txs (mapcat
                  (fn [eid]
                    (when-let [block (db/entity eid)]
-                     (when (get block property-id)
-                       (let [value (get block property-id)
-                             block-value? (and (= :default (get-in property [:block/schema :type] :default))
-                                               (uuid? value))
-                             property-block (when block-value? (db/entity [:block/uuid value]))
-                             retract-blocks-tx (when (and property-block
-                                                          (some? (get property-block :logseq.property/created-from-block))
-                                                          (some? (get property-block :logseq.property/created-from-property)))
-                                                 (let [txs-state (atom [])]
-                                                   (outliner-core/delete-block repo
-                                                                               (db/get-db false)
-                                                                               txs-state
-                                                                               (outliner-core/->Block property-block)
-                                                                               {:children? true})
-                                                   @txs-state))]
-                         (concat
-                          [[:db/retract (:db/id block) property-id]]
-                          retract-blocks-tx)))))
+                     (let [value (get block property-id)
+                           block-value? (and (= :default (get-in property [:block/schema :type] :default))
+                                             (uuid? value))
+                           property-block (when block-value? (db/entity [:block/uuid value]))
+                           retract-blocks-tx (when (and property-block
+                                                        (some? (get property-block :logseq.property/created-from-block))
+                                                        (some? (get property-block :logseq.property/created-from-property)))
+                                               (let [txs-state (atom [])]
+                                                 (outliner-core/delete-block repo
+                                                                             (db/get-db false)
+                                                                             txs-state
+                                                                             (outliner-core/->Block property-block)
+                                                                             {:children? true})
+                                                 @txs-state))]
+                       (concat
+                        [[:db/retract (:db/id block) property-id]]
+                        retract-blocks-tx))))
                  block-eids)]
         (when (seq txs)
           (db/transact! repo txs {:outliner-op :save-block}))))))
@@ -616,17 +644,25 @@
                                                (dissoc schema :description))}
                                icon
                                (assoc :logseq.property/icon icon)))]
-                          (let [page (get-property-hidden-page property)
-                                page-tx (when-not (e/entity? page) page)
-                                page-id [:block/uuid (:block/uuid page)]
-                                new-block (db-property-util/build-closed-value-block block-id resolved-value page-id property {:icon icon
-                                                                                                                               :description description})
-                                new-values (vec (conj closed-values block-id))]
-                            (->> (cons page-tx [new-block
-                                                {:db/id (:db/id property)
-                                                 :block/schema (merge {:type property-type}
-                                                                      (assoc property-schema :values new-values))}])
-                                 (remove nil?))))]
+                          (let [hidden-tx
+                                (if (contains? db-property-type/ref-property-types (:type property-schema))
+                                  []
+                                  (let [page (get-property-hidden-page property)
+                                        new-block (db-property-util/build-closed-value-block block-id resolved-value [:block/uuid (:block/uuid page)]
+                                                                                             property {:icon icon
+                                                                                                       :description description})]
+                                    (cond-> []
+                                      (not (e/entity? page))
+                                      (conj page)
+                                      true
+                                      (conj new-block))))
+                                new-values (if (contains? db-property-type/ref-property-types (:type property-schema))
+                                             (vec (conj closed-values (:block/uuid (db/entity resolved-value))))
+                                             (vec (conj closed-values block-id)))]
+                            (conj hidden-tx
+                                  {:db/id (:db/id property)
+                                   :block/schema (merge {:type property-type}
+                                                        (assoc property-schema :values new-values))})))]
             {:block-id block-id
              :tx-data tx-data}))))))
 

+ 4 - 1
src/main/frontend/handler/editor.cljs

@@ -2574,7 +2574,10 @@
   "Select first or last block in viewpoint"
   [direction]
   (let [f (case direction :up last :down first)
-        block (->> (util/get-blocks-noncollapse)
+        container (if (some-> js/document.activeElement
+                        (.querySelector ".blocks-container"))
+                    js/document.activeElement js/document.body)
+        block (->> (util/get-blocks-noncollapse container)
                    (f))]
     (when block
       (util/scroll-to-block block)

+ 13 - 22
src/main/frontend/handler/history.cljs

@@ -1,10 +1,8 @@
 (ns ^:no-doc frontend.handler.history
-  (:require [frontend.config :as config]
-            [frontend.db :as db]
+  (:require [frontend.db :as db]
             [frontend.db.transact :as db-transact]
             [frontend.handler.editor :as editor]
             [frontend.handler.route :as route-handler]
-            [frontend.modules.editor.undo-redo :as undo-redo]
             [frontend.state :as state]
             [frontend.util :as util]
             [goog.dom :as gdom]
@@ -29,15 +27,14 @@
 
 (defn restore-app-state!
   [state]
-  (when-not (:history/page-only-mode? @state/state)
-   (let [route-match (:route-match state)
-         current-route (:route-match @state/state)
-         prev-route-data (get-route-data route-match)
-         current-route-data (get-route-data current-route)]
-     (when (and (not= prev-route-data current-route-data)
-                prev-route-data)
-       (route-handler/redirect! prev-route-data))
-     (swap! state/state merge state))))
+  (let [route-match (:route-match state)
+        current-route (:route-match @state/state)
+        prev-route-data (get-route-data route-match)
+        current-route-data (get-route-data current-route)]
+    (when (and (not= prev-route-data current-route-data)
+               prev-route-data)
+      (route-handler/redirect! prev-route-data))
+    (swap! state/state merge state)))
 
 (defn undo!
   [e]
@@ -49,11 +46,8 @@
        (editor/save-current-block!)
        (state/clear-editor-action!)
        (state/set-block-op-type! nil)
-       (if (config/db-based-graph? repo)
-         (let [^js worker @state/*db-worker]
-           (.undo worker repo))
-         (let [cursor-state (undo-redo/undo)]
-           (state/set-state! :ui/restore-cursor-state (select-keys cursor-state [:editor-cursor :app-state]))))))))
+       (let [^js worker @state/*db-worker]
+         (.undo worker repo))))))
 
 (defn redo!
   [e]
@@ -61,8 +55,5 @@
     (when (db-transact/request-finished?)
       (util/stop e)
       (state/clear-editor-action!)
-      (if (config/db-based-graph? repo)
-        (let [^js worker @state/*db-worker]
-          (.redo worker repo))
-        (let [cursor-state (undo-redo/redo)]
-          (state/set-state! :ui/restore-cursor-state (select-keys cursor-state [:editor-cursor :app-state])))))))
+      (let [^js worker @state/*db-worker]
+        (.redo worker repo)))))

+ 1 - 1
src/main/frontend/handler/property/util.cljs

@@ -24,7 +24,7 @@
                         (name key)
                         key)]
     (if (sqlite-util/db-based-graph? repo)
-      (when-let [property (d/entity db (db-property/user-property-ident-from-name property-name))]
+      (when-let [property (d/entity db (db-property/create-user-property-ident-from-name property-name))]
         (get coll (:block/uuid property)))
       (get coll key))))
 

+ 0 - 211
src/main/frontend/modules/editor/undo_redo.cljs

@@ -1,211 +0,0 @@
-(ns frontend.modules.editor.undo-redo
-  (:require [frontend.db :as db]
-            [frontend.handler.notification :as notification]
-            [frontend.util.page :as page-util]
-            [frontend.state :as state]
-            [clojure.set :as set]
-            [medley.core :as medley]
-            [promesa.core :as p]))
-
-;;;; APIs
-
-(def ^:private undo-redo-states (atom {}))
-(def *pause-listener (atom false))
-
-(defn get-state
-  []
-  (let [repo (state/get-current-repo)]
-    (assert (string? repo) "Repo should satisfy string?")
-    (if-let [state (get @undo-redo-states repo)]
-      state
-      (let [new-state {:undo-stack (atom [])
-                       :redo-stack (atom [])}]
-        (swap! undo-redo-states assoc repo new-state)
-        new-state))))
-
-(defn- get-undo-stack
-  []
-  (-> (get-state) :undo-stack))
-
-(defn- get-redo-stack
-  []
-  (-> (get-state) :redo-stack))
-
-(defn push-undo
-  [txs]
-  (let [undo-stack (get-undo-stack)]
-    (swap! undo-stack conj txs)))
-
-(defn pop-undo
-  []
-  (let [undo-stack (get-undo-stack)]
-    (when-let [stack @undo-stack]
-      (when (seq stack)
-        (let [removed-e (peek stack)
-              popped-stack (pop stack)]
-          (reset! undo-stack popped-stack)
-          removed-e)))))
-
-(defn push-redo
-  [txs]
-  (let [redo-stack (get-redo-stack)]
-    (swap! redo-stack conj txs)))
-
-(defn pop-redo
-  []
-  (let [redo-stack (get-redo-stack)]
-    (when-let [removed-e (peek @redo-stack)]
-      (swap! redo-stack pop)
-      removed-e)))
-
-(defn page-pop-redo
-  [page-id]
-  (prn "[debug] redo: " (:block/original-name (db/pull page-id)))
-  (when-let [redo-stack (get-redo-stack)]
-    (when-let [stack @redo-stack]
-      (when (seq stack)
-        (let [reversed-stack (medley/indexed (reverse stack))
-              idx (some (fn [[idx item]]
-                          (some #(when (or (= (:db/id %) page-id)
-                                           (= (:db/id (:block/page %)) page-id)) idx) (:blocks item))) reversed-stack)]
-          (when idx
-            (let [idx' (- (count stack) idx 1)
-                  before (subvec stack 0 idx')
-                  after (subvec stack (inc idx'))
-                  others (vec (concat before after))]
-              (reset! redo-stack others)
-              (prn "[debug] redo remove: " (nth stack idx'))
-              (nth stack idx'))))))))
-
-(defn- smart-pop-redo
-  []
-  (if (:history/page-only-mode? @state/state)
-    (if-let [page-id (page-util/get-editing-page-id)]
-      (page-pop-redo page-id)
-      (pop-redo))
-    (pop-redo)))
-
-(defn reset-redo
-  []
-  (let [redo-stack (get-redo-stack)]
-    (reset! redo-stack [])))
-
-(defn get-txs
-  [redo? txs]
-  (let [txs (if redo? txs (reverse txs))]
-    (mapv (fn [[id attr value tx add?]]
-            (let [op (cond
-                       (and redo? add?) :db/add
-                       (and (not redo?) add?) :db/retract
-                       (and redo? (not add?)) :db/retract
-                       (and (not redo?) (not add?)) :db/add)]
-              [op id attr value tx]))
-          txs)))
-
-;;;; Invokes
-
-(defn- transact!
-  [txs tx-meta]
-  (db/transact! (state/get-current-repo) txs tx-meta))
-
-(defn- page-pop-undo
-  [page-id]
-  (let [undo-stack (get-undo-stack)]
-    (when-let [stack @undo-stack]
-      (when (seq stack)
-        (let [reversed-stack (medley/indexed (reverse stack))
-              idx (some (fn [[idx item]]
-                          (some #(when (or (= (:db/id %) page-id)
-                                           (= (:db/id (:block/page %)) page-id)) idx) (:blocks item))) reversed-stack)]
-          (when idx
-            (let [idx' (- (count stack) idx 1)
-                  before (subvec stack 0 idx')
-                  after (subvec stack (inc idx'))
-                  others (vec (concat before after))]
-              (reset! undo-stack others)
-              (prn "[debug] undo remove: " (nth stack idx'))
-              (nth stack idx'))))))))
-
-(defn- smart-pop-undo
-  []
-  (if (:history/page-only-mode? @state/state)
-    (if-let [page-id (page-util/get-editing-page-id)]
-      (page-pop-undo page-id)
-      (pop-undo))
-    (pop-undo)))
-
-(defn pause-listener!
-  []
-  (reset! *pause-listener true))
-
-(defn resume-listener!
-  []
-  (reset! *pause-listener false))
-
-(defn undo
-  []
-  (when-let [e (smart-pop-undo)]
-    (pause-listener!)
-    (state/set-editor-op! :undo)
-    (let [{:keys [txs tx-meta tx-id]} e
-          new-txs (get-txs false txs)
-          editor-cursor (:before (get @(get @state/state :history/tx->editor-cursor) tx-id))]
-      (push-redo e)
-      (p/do!
-       (transact! new-txs (assoc tx-meta :undo? true)))
-      (assoc e
-             :txs-op new-txs
-             :editor-cursor editor-cursor))))
-
-(defn redo
-  []
-  (when-let [{:keys [txs tx-meta tx-id] :as e} (smart-pop-redo)]
-    (pause-listener!)
-    (state/set-editor-op! :redo)
-    (let [new-txs (get-txs true txs)
-          editor-cursor (let [s (get @(get @state/state :history/tx->editor-cursor) tx-id)]
-                          (if (= (:outliner-op tx-meta) :save-block)
-                            (:before s)
-                            (or (:after s) (:before s))))]
-      (push-undo e)
-      (p/do!
-       (transact! new-txs (assoc tx-meta :redo? true)))
-
-      (assoc e
-             :txs-op new-txs
-             :editor-cursor editor-cursor))))
-
-(defn toggle-undo-redo-mode!
-  []
-  (swap! state/state update :history/page-only-mode? not)
-  (let [mode (if (:history/page-only-mode? @state/state) "Page only" "Global")]
-    (notification/show!
-     [:p (str "Undo/redo mode: " mode)])))
-
-
-(defn listen-db-changes!
-  [{:keys [tx-id tx-data tx-meta blocks pages]}]
-  (when (and (seq tx-data)
-             (not (or (:undo? tx-meta)
-                      (:redo? tx-meta)))
-             (not @*pause-listener)
-             (not (set/subset?
-                   (set (map :a tx-data))
-                   #{:block/created-at :block/updated-at})))
-    (reset-redo)
-    (if (:replace? tx-meta)
-      (when-let [removed-e (pop-undo)]
-        (let [entity (update removed-e :txs concat tx-data)]
-          (push-undo entity)))
-      (let [updated-blocks (concat blocks pages)
-            entity {:blocks updated-blocks
-                    :tx-id tx-id
-                    :txs tx-data
-                    :tx-meta tx-meta
-                    :app-state (select-keys @state/state
-                                            [:route-match
-                                             :ui/sidebar-open?
-                                             :ui/sidebar-collapsed-blocks
-                                             :sidebar/blocks])}]
-        (push-undo entity))))
-  (resume-listener!))

+ 3 - 16
src/main/frontend/modules/outliner/pipeline.cljs

@@ -1,22 +1,12 @@
 (ns frontend.modules.outliner.pipeline
-  (:require [frontend.config :as config]
-            [frontend.db :as db]
+  (:require [frontend.db :as db]
             [frontend.db.react :as react]
             [frontend.state :as state]
-            [frontend.modules.editor.undo-redo :as undo-redo]
             [datascript.core :as d]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.history :as history]
             [frontend.util :as util]))
 
-(defn store-undo-data!
-  [{:keys [tx-meta] :as opts}]
-  (when-not config/test?
-    (when (or (:outliner/transact? tx-meta)
-              (:outliner-op tx-meta)
-              (:whiteboard/transact? tx-meta))
-      (undo-redo/listen-db-changes! opts))))
-
 (defn- get-tx-id
   [tx-report]
   (get-in tx-report [:tempids :db/current-tx]))
@@ -35,12 +25,12 @@
   (history/restore-app-state! app-state))
 
 (defn invoke-hooks
-  [{:keys [_request-id tx-meta tx-data deleted-block-uuids affected-keys blocks] :as opts}]
+  [{:keys [_request-id tx-meta tx-data deleted-block-uuids affected-keys blocks]}]
   ;; (prn :debug
   ;;      :request-id request-id
   ;;      :tx-meta tx-meta
   ;;      :tx-data tx-data)
-  (let [{:keys [from-disk? new-graph? local-tx? undo? redo? initial-pages? end?]} tx-meta
+  (let [{:keys [from-disk? new-graph? undo? redo? initial-pages? end?]} tx-meta
         repo (state/get-current-repo)
         tx-report {:tx-meta tx-meta
                    :tx-data tx-data}
@@ -69,9 +59,6 @@
                            (concat update-blocks-fully-loaded tx-data))
                          tx-data)
               tx-report (d/transact! conn tx-data' tx-meta)]
-          (when local-tx?
-            (let [tx-id (get-tx-id tx-report)]
-              (store-undo-data! (assoc opts :tx-id tx-id))))
           (when-not (or undo? redo?)
             (update-current-tx-editor-cursor! tx-report)))
 

+ 0 - 6
src/main/frontend/modules/shortcut/config.cljs

@@ -19,7 +19,6 @@
             [frontend.handler.plugin-config :as plugin-config-handler]
             [frontend.handler.window :as window-handler]
             [frontend.handler.jump :as jump-handler]
-            [frontend.modules.editor.undo-redo :as undo-redo]
             [frontend.dicts :as dicts]
             [frontend.modules.shortcut.before :as m]
             [frontend.state :as state]
@@ -360,9 +359,6 @@
    :editor/zoom-out                         {:binding (if mac? "mod+," "alt+left")
                                              :fn      editor-handler/zoom-out!}
 
-   :editor/toggle-undo-redo-mode            {:binding []
-                                             :fn      undo-redo/toggle-undo-redo-mode!}
-
    :editor/toggle-number-list               {:binding "t n"
                                              :fn      #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
 
@@ -722,7 +718,6 @@
      (-> (build-category-map
            [:editor/insert-link
             :editor/select-all-blocks
-            :editor/toggle-undo-redo-mode
             :editor/toggle-number-list
             :editor/undo
             :editor/redo
@@ -888,7 +883,6 @@
      :shortcut.category/toggle
      [:ui/toggle-help
       :editor/toggle-open-blocks
-      :editor/toggle-undo-redo-mode
       :editor/toggle-number-list
       :ui/toggle-wide-mode
       :ui/toggle-document-mode

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

@@ -307,7 +307,6 @@
       :whiteboard/onboarding-tour?           (or (storage/get :whiteboard-onboarding-tour?) false)
       :whiteboard/last-persisted-at          {}
       :whiteboard/pending-tx-data            {}
-      :history/page-only-mode?               false
       :history/tx-before-editor-cursor       (atom nil)
       ;; db tx-id -> editor cursor
       :history/tx->editor-cursor             (atom {})

+ 0 - 8
src/main/frontend/util/page.cljs

@@ -17,14 +17,6 @@
   (let [page-name (state/get-current-page)]
     (:db/id (db/get-page page-name))))
 
-(defn get-editing-page-id
-  "Fetch the editing page id. If there is an edit-input-id set, we are probably still
-   on editing mode"
-  []
-  (if (or (state/editing?) (state/get-edit-input-id))
-    (get-in (first (state/get-editor-args)) [:block :block/page :db/id])
-    (get-current-page-id)))
-
 (defn get-page-file-rpath
   "Gets the file path of a page. If no page is given, detects the current page.
 Returns nil if no file path is found or no page is detected or given"

+ 3 - 10
src/main/frontend/worker/batch_tx.clj

@@ -4,15 +4,8 @@
 (defmacro with-batch-tx-mode
   "1. start batch-tx mode
   2. run body
-  3. exit batch-tx mode
-  4. refresh-ui"
-  [conn & body]
+  3. exit batch-tx mode"
+  [& body]
   `(do (frontend.worker.batch-tx/start-batch-tx-mode)
        ~@body
-       (let [txs# (frontend.worker.batch-tx/get-batch-txs)]
-         (frontend.worker.batch-tx/exit-batch-tx-mode)
-         (when (seq txs#)
-           (when-let [affected-keys# (not-empty
-                                      (frontend.worker.react/get-affected-queries-keys
-                                       {:db-after @~conn :tx-data txs#}))]
-             (frontend.worker.util/post-message :refresh-ui {:affected-keys affected-keys#}))))))
+       (frontend.worker.batch-tx/exit-batch-tx-mode)))

+ 15 - 14
src/main/frontend/worker/pipeline.cljs

@@ -74,13 +74,11 @@
              (not (:node-test? context)))
     (fix-db! conn tx-report)))
 
-(defonce *enable-db-validate? (atom false))
-
 (defn invoke-hooks
   [repo conn tx-report context]
   (when-not (:pipeline-replace? (:tx-meta tx-report))
     (let [tx-meta (:tx-meta tx-report)
-          {:keys [from-disk? new-graph? undo? redo?]} tx-meta
+          {:keys [from-disk? new-graph?]} tx-meta
           {:keys [pages blocks]} (ds-report/get-blocks-and-pages tx-report)]
       (if (or from-disk? new-graph?)
         (let [path-refs (set (compute-block-path-refs-tx tx-report blocks))
@@ -103,14 +101,13 @@
                       (when (d/entity @conn page-id)
                         (file/sync-to-file repo page-id tx-meta)))))
               deleted-block-uuids (set (outliner-pipeline/filter-deleted-blocks (:tx-data tx-report)))
-              replace-tx (when-not (or undo? redo?)
-                           (concat
+              replace-tx (concat
                             ;; block path refs
-                            (set (compute-block-path-refs-tx tx-report blocks))
+                          (set (compute-block-path-refs-tx tx-report blocks))
 
                             ;; delete empty property parent block
-                            (when (seq deleted-block-uuids)
-                              (delete-property-parent-block-if-empty tx-report deleted-block-uuids))
+                          (when (seq deleted-block-uuids)
+                            (delete-property-parent-block-if-empty tx-report deleted-block-uuids))
 
                             ;; update block/tx-id
                             (let [updated-blocks (remove (fn [b] (contains? (set deleted-block-uuids) (:block/uuid b)))
@@ -121,7 +118,7 @@
                                       (when-let [db-id (:db/id b)]
                                         {:db/id db-id
                                          :block/tx-id tx-id})) updated-blocks)
-                               (remove nil?)))))
+                               (remove nil?))))
               tx-report' (or
                           (when (seq replace-tx)
                             ;; TODO: remove this since transact! is really slow
@@ -130,14 +127,18 @@
                           (do
                             (when-not (exists? js/process) (d/store @conn))
                             tx-report))
-              fix-tx-data (when @*enable-db-validate? (validate-and-fix-db! repo conn tx-report context))
-              full-tx-data (concat (:tx-data tx-report) fix-tx-data (:tx-data tx-report'))
+              fix-tx-data (validate-and-fix-db! repo conn tx-report context)
+              batch-processing? (batch-tx/tx-batch-processing?)
+              batch-tx-data (batch-tx/get-batch-txs)
+              full-tx-data (concat (:tx-data tx-report)
+                                   fix-tx-data
+                                   (:tx-data tx-report')
+                                   (when (and (not batch-processing?) (seq batch-tx-data))
+                                     batch-tx-data))
               final-tx-report (assoc tx-report'
                                      :tx-data full-tx-data
                                      :db-before (:db-before tx-report))
-              batch-processing? (batch-tx/tx-batch-processing?)
-              affected-query-keys (when-not (or (:importing? context)
-                                                batch-processing?)
+              affected-query-keys (when-not (:importing? context)
                                     (worker-react/get-affected-queries-keys final-tx-report))]
           (when batch-processing?
             (batch-tx/conj-batch-txs! full-tx-data))

+ 132 - 83
src/main/frontend/worker/undo_redo.cljs

@@ -40,6 +40,12 @@ when undo this op, this original entity-map will be transacted back into db")
 (sr/defkeyword ::update-block
   "when a block is updated, generate a ::update-block undo-op.")
 
+(sr/defkeyword ::empty-undo-stack
+  "return by undo, when no more undo ops")
+
+(sr/defkeyword ::empty-redo-stack
+  "return by redo, when no more redo ops")
+
 (def ^:private boundary [::boundary])
 
 (def ^:private undo-op-schema
@@ -81,6 +87,10 @@ when undo this op, this original entity-map will be transacted back into db")
 
 (def ^:private undo-ops-validator (m/validator [:sequential undo-op-schema]))
 
+(def ^:dynamic *undo-redo-info-for-test*
+  "record undo-op info when running-test"
+  nil)
+
 (def ^:private entity-map-pull-pattern
   [:block/uuid
    {:block/left [:block/uuid]}
@@ -151,7 +161,7 @@ when undo this op, this original entity-map will be transacted back into db")
 
               :else
               (recur (dec i) (conj r peek-op)))))]
-    [ops (subvec stack 0 i)]))
+    [ops (subvec (vec stack) 0 i)]))
 
 (defn- pop-undo-ops
   [repo]
@@ -161,6 +171,14 @@ when undo this op, this original entity-map will be transacted back into db")
     (swap! repo->undo-stack assoc repo undo-stack*)
     ops))
 
+(defn- empty-undo-stack?
+  [repo]
+  (empty? (@(:undo/repo->undo-stack @worker-state/*state) repo)))
+
+(defn- empty-redo-stack?
+  [repo]
+  (empty? (@(:undo/repo->redo-stack @worker-state/*state) repo)))
+
 (defn- push-redo-ops
   [repo ops]
   (assert (undo-ops-validator ops) ops)
@@ -174,48 +192,57 @@ when undo this op, this original entity-map will be transacted back into db")
     (swap! repo->redo-stack assoc repo redo-stack*)
     ops))
 
+(defn- normal-block?
+  [entity]
+  (and (:block/parent entity)
+       (:block/left entity)))
+
 (defmulti ^:private reverse-apply-op (fn [op _conn _repo] (first op)))
 (defmethod reverse-apply-op ::remove-block
   [op conn repo]
-  (let [[_ {:keys [block-uuid block-entity-map]}] op]
-    (when-let [left-entity (d/entity @conn [:block/uuid (:block/left block-entity-map)])]
-      (let [sibling? (not= (:block/left block-entity-map) (:block/parent block-entity-map))]
-        (outliner-tx/transact!
-         {:gen-undo-op? false
-          :outliner-op :insert-blocks
-          :transact-opts {:repo repo
-                          :conn conn}}
-         (outliner-core/insert-blocks! repo conn
-                                       [(cond-> {:block/uuid block-uuid
-                                                 :block/content (:block/content block-entity-map)
-                                                 :block/format :markdown}
-                                          (:block/created-at block-entity-map)
-                                          (assoc :block/created-at (:block/created-at block-entity-map))
-
-                                          (:block/updated-at block-entity-map)
-                                          (assoc :block/updated-at (:block/updated-at block-entity-map))
-
-                                          (seq (:block/tags block-entity-map))
-                                          (assoc :block/tags (mapv (partial vector :block/uuid)
-                                                                   (:block/tags block-entity-map))))]
-                                       left-entity {:sibling? sibling? :keep-uuid? true}))
-        :push-undo-redo))))
+  (let [[_ {:keys [block-uuid block-entity-map]}] op
+        block-entity (d/entity @conn [:block/uuid block-uuid])]
+    (when-not block-entity ;; this block shouldn't exist now
+      (when-let [left-entity (d/entity @conn [:block/uuid (:block/left block-entity-map)])]
+        (let [sibling? (not= (:block/left block-entity-map) (:block/parent block-entity-map))]
+          (some->>
+           (outliner-tx/transact!
+            {:gen-undo-op? false
+             :outliner-op :insert-blocks
+             :transact-opts {:repo repo
+                             :conn conn}}
+            (outliner-core/insert-blocks! repo conn
+                                          [(cond-> {:block/uuid block-uuid
+                                                    :block/content (:block/content block-entity-map)
+                                                    :block/format :markdown}
+                                             (:block/created-at block-entity-map)
+                                             (assoc :block/created-at (:block/created-at block-entity-map))
+
+                                             (:block/updated-at block-entity-map)
+                                             (assoc :block/updated-at (:block/updated-at block-entity-map))
+
+                                             (seq (:block/tags block-entity-map))
+                                             (assoc :block/tags (mapv (partial vector :block/uuid)
+                                                                      (:block/tags block-entity-map))))]
+                                          left-entity {:sibling? sibling? :keep-uuid? true}))
+           (conj [:push-undo-redo])))))))
 
 (defmethod reverse-apply-op ::insert-block
   [op conn repo]
   (let [[_ {:keys [block-uuid]}] op]
     (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
-      (when (empty? (seq (:block/_parent block-entity))) ;if have children, skip
-        (outliner-tx/transact!
-         {:gen-undo-op? false
-          :outliner-op :delete-blocks
-          :transact-opts {:repo repo
-                          :conn conn}}
-         (outliner-core/delete-blocks! repo conn
-                                       (common-config/get-date-formatter (worker-state/get-config repo))
-                                       [block-entity]
-                                       {:children? false}))
-        :push-undo-redo))))
+      (when (empty? (:block/_parent block-entity)) ;if have children, skip
+        (some->>
+         (outliner-tx/transact!
+          {:gen-undo-op? false
+           :outliner-op :delete-blocks
+           :transact-opts {:repo repo
+                           :conn conn}}
+          (outliner-core/delete-blocks! repo conn
+                                        (common-config/get-date-formatter (worker-state/get-config repo))
+                                        [block-entity]
+                                        {:children? false}))
+         (conj [:push-undo-redo]))))))
 
 (defmethod reverse-apply-op ::move-block
   [op conn repo]
@@ -223,65 +250,72 @@ when undo this op, this original entity-map will be transacted back into db")
     (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
       (when-let [left-entity (d/entity @conn [:block/uuid block-origin-left])]
         (let [sibling? (not= block-origin-left block-origin-parent)]
-          (outliner-tx/transact!
-           {:gen-undo-op? false
-            :outliner-op :move-blocks
-            :transact-opts {:repo repo
-                            :conn conn}}
-           (outliner-core/move-blocks! repo conn [block-entity] left-entity sibling?))
-          :push-undo-redo)))))
+          (some->>
+           (outliner-tx/transact!
+            {:gen-undo-op? false
+             :outliner-op :move-blocks
+             :transact-opts {:repo repo
+                             :conn conn}}
+            (outliner-core/move-blocks! repo conn [block-entity] left-entity sibling?))
+           (conj [:push-undo-redo])))))))
 
 (defmethod reverse-apply-op ::update-block
   [op conn repo]
   (let [[_ {:keys [block-uuid block-origin-content]}] op]
     (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
-      (let [new-block (assoc block-entity :block/content block-origin-content)]
-        (outliner-tx/transact!
-         {:gen-undo-op? false
-          :outliner-op :save-block
-          :transact-opts {:repo repo
-                          :conn conn}}
-         (outliner-core/save-block! repo conn
-                                    (common-config/get-date-formatter (worker-state/get-config repo))
-                                    new-block))
-        :push-undo-redo))))
+      (when (normal-block? block-entity)
+        (let [new-block (assoc block-entity :block/content block-origin-content)]
+          (some->>
+           (outliner-tx/transact!
+            {:gen-undo-op? false
+             :outliner-op :save-block
+             :transact-opts {:repo repo
+                             :conn conn}}
+            (outliner-core/save-block! repo conn
+                                       (common-config/get-date-formatter (worker-state/get-config repo))
+                                       new-block))
+           (conj [:push-undo-redo])))))))
 
 (defn undo
-  [repo]
+  [repo conn]
   (if-let [ops (not-empty (pop-undo-ops repo))]
-    (let [conn (worker-state/get-datascript-conn repo)
-          redo-ops-to-push (transient [])]
+    (let [redo-ops-to-push (transient [])]
       (batch-tx/with-batch-tx-mode conn
         (doseq [op ops]
-          (let [rev-op (reverse-op @conn op)]
-            (when (= :push-undo-redo (reverse-apply-op op conn repo))
+          (let [rev-op (reverse-op @conn op)
+                r (reverse-apply-op op conn repo)]
+            (when (= :push-undo-redo (first r))
+              (some-> *undo-redo-info-for-test* (reset! {:op op :tx (second r)}))
               (conj! redo-ops-to-push rev-op)))))
       (when-let [rev-ops (not-empty (persistent! redo-ops-to-push))]
-        (push-redo-ops repo (cons boundary rev-ops))))
-    (prn "No further undo information")))
+        (push-redo-ops repo (cons boundary rev-ops)))
+      nil)
+
+    (when (empty-undo-stack? repo)
+      (prn "No further undo information")
+      ::empty-undo-stack)))
 
 (defn redo
-  [repo]
+  [repo conn]
   (if-let [ops (not-empty (pop-redo-ops repo))]
-    (let [conn (worker-state/get-datascript-conn repo)
-          undo-ops-to-push (transient [])]
+    (let [undo-ops-to-push (transient [])]
       (batch-tx/with-batch-tx-mode conn
         (doseq [op ops]
-          (let [rev-op (reverse-op @conn op)]
-            (when (= :push-undo-redo (reverse-apply-op op conn repo))
+          (let [rev-op (reverse-op @conn op)
+                r (reverse-apply-op op conn repo)]
+            (when (= :push-undo-redo (first r))
+              (some-> *undo-redo-info-for-test* (reset! {:op op :tx (second r)}))
               (conj! undo-ops-to-push rev-op)))))
       (when-let [rev-ops (not-empty (persistent! undo-ops-to-push))]
-        (push-undo-ops repo (cons boundary rev-ops))))
-    (prn "No further redo information")))
+        (push-undo-ops repo (cons boundary rev-ops)))
+      nil)
 
+    (when (empty-redo-stack? repo)
+      (prn "No further redo information")
+      ::empty-redo-stack)))
 
 ;;; listen db changes and push undo-ops
 
-(defn- normal-block?
-  [entity]
-  (and (:block/parent entity)
-       (:block/left entity)))
-
 (defn- entity-datoms=>ops
   [db-before db-after id->attr->datom entity-datoms]
   (when-let [e (ffirst entity-datoms)]
@@ -306,14 +340,27 @@ when undo this op, this original entity-map will be transacted back into db")
 
             (and (or add3? add4?)
                  (normal-block? entity-after))
-            (cond-> [[::move-block
-                      {:block-uuid (:block/uuid entity-after)
-                       :block-origin-left (:block/uuid (:block/left entity-before))
-                       :block-origin-parent (:block/uuid (:block/parent entity-before))}]]
-              (and add2? block-content)
-              (conj [::update-block
-                     {:block-uuid (:block/uuid entity-after)
-                      :block-origin-content (:block/content entity-before)}]))
+            (let [origin-left (:block/left entity-before)
+                  origin-parent (:block/parent entity-before)
+                  origin-left-in-db-after (d/entity db-after [:block/uuid (:block/uuid origin-left)])
+                  origin-parent-in-db-after (d/entity db-after [:block/uuid (:block/uuid origin-parent)])
+                  origin-left-and-parent-available-in-db-after?
+                  (and origin-left-in-db-after origin-parent-in-db-after
+                       (if (not= (:block/uuid origin-left) (:block/uuid origin-parent))
+                         (= (:block/uuid (:block/parent origin-left))
+                            (:block/uuid (:block/parent origin-left-in-db-after)))
+                         true))]
+              (cond-> []
+                origin-left-and-parent-available-in-db-after?
+                (conj [::move-block
+                       {:block-uuid (:block/uuid entity-after)
+                        :block-origin-left (:block/uuid (:block/left entity-before))
+                        :block-origin-parent (:block/uuid (:block/parent entity-before))}])
+
+                (and add2? block-content)
+                (conj [::update-block
+                       {:block-uuid (:block/uuid entity-after)
+                        :block-origin-content (:block/content entity-before)}])))
 
             (and add2? block-content
                  (normal-block? entity-after))
@@ -336,11 +383,13 @@ when undo this op, this original entity-map will be transacted back into db")
 
 ;;; listen db changes and push undo-ops (ends)
 
+(defn clear-undo-redo-stack
+  []
+  (reset! (:undo/repo->undo-stack @worker-state/*state) {})
+  (reset! (:undo/repo->redo-stack @worker-state/*state) {}))
+
 (comment
-  (defn- clear-undo-redo-stack
-    []
-    (reset! (:undo/repo->undo-stack @worker-state/*state) {})
-    (reset! (:undo/repo->redo-stack @worker-state/*state) {}))
+
   (clear-undo-redo-stack)
   (add-watch (:undo/repo->undo-stack @worker-state/*state)
              :xxx

+ 5 - 0
src/main/logseq/api.cljs

@@ -89,6 +89,7 @@
                      (subs % 1)
                      (keyword %)))
              (get-in @state/state)
+             (#(if (util/atom? %) @% %))
              (sdk-utils/normalize-keyword-for-json)
              (bean/->js))))
 
@@ -548,6 +549,10 @@
                                                   (db-model/query-block-by-uuid)))))]
         (bean/->js (sdk-utils/normalize-keyword-for-json blocks))))))
 
+(def ^:export clear_selected_blocks
+  (fn []
+    (state/clear-selection!)))
+
 (def ^:export get_current_page
   (fn []
     (when-let [page (state/get-current-page)]

+ 26 - 5
src/main/logseq/sdk/experiments.cljs

@@ -2,13 +2,34 @@
   (:require [frontend.state :as state]
             [frontend.components.page :as page]
             [frontend.util :as util]
+            [camel-snake-kebab.core :as csk]
+            [goog.object :as gobj]
             [frontend.handler.plugin :as plugin-handler]))
 
+(defn- jsx->clj
+  [^js obj]
+  (if (js/goog.isObject obj)
+    (-> (fn [result k]
+          (let [v (gobj/get obj k)
+                k (keyword (csk/->kebab-case k))]
+            (if (= "function" (goog/typeOf v))
+              (assoc result k v)
+              (assoc result k (jsx->clj v)))))
+      (reduce {} (gobj/getKeys obj)))
+    obj))
+
 (defn ^:export cp_page_editor
   [^js props]
-  (let [p (some-> props (aget "page"))]
-    (when-let [e (page/get-page-entity p)]
-      (page/page-blocks-cp (state/get-current-repo) e {}))))
+  (let [props1 (jsx->clj props)
+        page-name (some-> props1 :page)
+        config (some-> props1 (dissoc :page))]
+    (when-let [_entity (page/get-page-entity page-name)]
+      (page/page
+        {:repo (state/get-current-repo)
+         :page-name page-name
+         :preview? false
+         :sidebar? false
+         :config config}))))
 
 (defn ^:export register_fenced_code_renderer
   [pid type ^js opts]
@@ -34,10 +55,10 @@
   (when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
     (plugin-handler/register-daemon-renderer
       (keyword pid) key (reduce #(assoc %1 %2 (aget opts (name %2))) {}
-                           [:before :subs :render]))))
+                          [:before :subs :render]))))
 
 (defn ^:export register_extensions_enhancer
   [pid type enhancer]
   (when-let [^js _pl (and (fn? enhancer) (plugin-handler/get-plugin-inst pid))]
     (plugin-handler/register-extensions-enhancer
-      (keyword pid) type {:enhancer enhancer})))
+      (keyword pid) type {:enhancer enhancer})))

+ 0 - 1
src/resources/dicts/en.edn

@@ -726,7 +726,6 @@
   :editor/select-parent           "Select parent block"
   :editor/zoom-in                 "Zoom in editing block / Forwards otherwise"
   :editor/zoom-out                "Zoom out editing block / Backwards otherwise"
-  :editor/toggle-undo-redo-mode   "Toggle undo redo mode (global or page only)"
   :editor/toggle-number-list      "Toggle number list"
   :editor/add-property            "Add property"
   :editor/jump                    "Jump to a property key or value"

+ 27 - 3
src/test/frontend/handler/db_based/property_test.cljs

@@ -178,11 +178,13 @@
       (db-property-handler/class-add-property! repo c1id :user.property/property-2)
       ;; repeated adding property-2
       (db-property-handler/class-add-property! repo c1id :user.property/property-2)
-      (is (= 2 (count (:class/schema.properties (db/entity (:db/id c1)))))))
+      ;; add new property with same base db-ident as property-1
+      (db-property-handler/class-add-property! repo c1id ":property-1")
+      (is (= 3 (count (:class/schema.properties (db/entity (:db/id c1)))))))
 
     (testing "Class remove property"
       (db-property-handler/class-remove-property! repo c1id :user.property/property-1)
-      (is (= 1 (count (:class/schema.properties (db/entity (:db/id c1)))))))
+      (is (= 2 (count (:class/schema.properties (db/entity (:db/id c1)))))))
     (testing "Add classes to a block"
       (test-helper/save-block! repo fbid "Block 1" {:tags ["class1" "class2" "class3"]})
       (is (= 3 (count (:block/tags (db/entity [:block/uuid fbid]))))))
@@ -199,7 +201,7 @@
                         :class/parent (:db/id c2)}]))
       (db-property-handler/class-add-property! repo c2id :user.property/property-3)
       (db-property-handler/class-add-property! repo c2id :user.property/property-4)
-      (is (= 3 (count (:classes-properties
+      (is (= 4 (count (:classes-properties
                        (db-property-handler/get-block-classes-properties (:db/id (db/entity [:block/uuid fbid]))))))))))
 
 
@@ -253,6 +255,28 @@
         (db-property-handler/collapse-expand-property! repo fb property false)
         (is (nil? (:block/collapsed-properties (db/entity [:block/uuid fbid]))))))))
 
+(deftest upsert-property!
+  (testing "Update an existing property"
+    (let [repo (state/get-current-repo)]
+      (db-property-handler/upsert-property! repo nil {:type :default} {:property-name "p0"})
+      (db-property-handler/upsert-property! repo :user.property/p0 {:type :default :cardinality :many} {})
+      (is (= :many (get-in (db/entity repo :user.property/p0) [:block/schema :cardinality])))))
+  (testing "Multiple properties that generate the same initial :db/ident"
+    (let [repo (state/get-current-repo)]
+      (db-property-handler/upsert-property! repo nil {:type :default} {:property-name "p1"})
+      (db-property-handler/upsert-property! repo nil {} {:property-name ":p1"})
+      (db-property-handler/upsert-property! repo nil {} {:property-name "1p1"})
+
+      (is (= {:block/name "p1" :block/original-name "p1" :block/schema {:type :default}}
+             (select-keys (db/entity repo :user.property/p1) [:block/name :block/original-name :block/schema]))
+          "Existing db/ident does not get modified")
+      (is (= ":p1"
+             (:block/original-name (db/entity repo :user.property/p1-1)))
+          "2nd property gets unique ident")
+      (is (= "1p1"
+             (:block/original-name (db/entity repo :user.property/p1-2)))
+          "3rd property gets unique ident"))))
+
 ;; template (TBD, template implementation not settle down yet)
 ;; property-create-new-block-from-template
 

+ 30 - 0
src/test/frontend/test/generators.cljs

@@ -0,0 +1,30 @@
+(ns frontend.test.generators
+  "Generators for block-related data"
+  (:require [clojure.test.check.generators :as gen]
+            [datascript.core :as d]))
+
+
+(defn gen-available-block-uuid
+  [db]
+  (gen/elements
+   (->> (d/q '[:find ?block-uuid
+               :where
+               [?block :block/parent]
+               [?block :block/left]
+               [?block :block/uuid ?block-uuid]]
+             db)
+        (apply concat))))
+
+
+(defn gen-available-parent-left-pair
+  "generate [<parent-uuid> <left-uuid>]"
+  [db]
+  (gen/elements
+   (d/q '[:find ?parent-uuid ?left-uuid
+          :where
+          [?b :block/uuid]
+          [?b :block/parent ?parent]
+          [?b :block/left ?left]
+          [?parent :block/uuid ?parent-uuid]
+          [?left :block/uuid ?left-uuid]]
+        db)))

+ 163 - 7
src/test/frontend/worker/undo_redo_test.cljs

@@ -1,11 +1,167 @@
 (ns frontend.worker.undo-redo-test
-  (:require [frontend.worker.undo-redo :as undo-redo]
-            [clojure.test :refer [deftest]]))
+  (:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
+            [clojure.test.check.generators :as gen]
+            [datascript.core :as d]
+            [frontend.db :as db]
+            [frontend.test.generators :as t.gen]
+            [frontend.test.helper :as test-helper]
+            [frontend.worker.undo-redo :as undo-redo]))
 
+(def ^:private init-data (test-helper/initial-test-page-and-blocks))
+(defn- start-and-destroy-db
+  [f]
+  (test-helper/db-based-start-and-destroy-db
+   f
+   {:init-data (fn [conn] (d/transact! conn init-data))}))
 
+(use-fixtures :each start-and-destroy-db)
 
-(deftest reverse-op-test
-  ;; TODO: add tests for undo-redo
-  undo-redo/undo
-  undo-redo/redo
-  )
+(def ^:private gen-non-exist-block-uuid gen/uuid)
+
+(defn- gen-block-uuid
+  [db & {:keys [non-exist-frequency] :or {non-exist-frequency 1}}]
+  (gen/frequency [[9 (t.gen/gen-available-block-uuid db)] [non-exist-frequency gen-non-exist-block-uuid]]))
+
+(defn- gen-parent-left-pair
+  [db]
+  (gen/frequency [[9 (t.gen/gen-available-parent-left-pair db)] [1 (gen/vector gen-non-exist-block-uuid 2)]]))
+
+(defn- gen-move-block-op
+  [db]
+  (gen/let [block-uuid (gen-block-uuid db)
+            [parent left] (gen-parent-left-pair db)]
+    [:frontend.worker.undo-redo/move-block
+     {:block-uuid block-uuid
+      :block-origin-left left
+      :block-origin-parent parent}]))
+
+(defn- gen-insert-block-op
+  [db]
+  (gen/let [block-uuid (gen-block-uuid db)]
+    [:frontend.worker.undo-redo/insert-block
+     {:block-uuid block-uuid}]))
+
+(defn- gen-remove-block-op
+  [db]
+  (gen/let [block-uuid (gen-block-uuid db {:non-exist-frequency 80})
+            [parent left] (gen-parent-left-pair db)
+            content gen/string-alphanumeric]
+    [:frontend.worker.undo-redo/remove-block
+     {:block-uuid block-uuid
+      :block-entity-map
+      {:block/uuid block-uuid
+       :block/left left
+       :block/parent parent
+       :block/content content}}]))
+
+(defn- gen-update-block-op
+  [db]
+  (gen/let [block-uuid (gen-block-uuid db)
+            content gen/string-alphanumeric]
+    [:frontend.worker.undo-redo/update-block
+     {:block-uuid block-uuid
+      :block-origin-content content}]))
+
+(def ^:private gen-boundary (gen/return [:frontend.worker.undo-redo/boundary]))
+
+(defn- gen-op
+  [db & {:keys [insert-block-op move-block-op remove-block-op update-block-op boundary-op]
+         :or {insert-block-op 2
+              move-block-op 2
+              remove-block-op 4
+              update-block-op 2
+              boundary-op 2}}]
+  (gen/frequency [[insert-block-op (gen-insert-block-op db)]
+                  [move-block-op (gen-move-block-op db)]
+                  [remove-block-op (gen-remove-block-op db)]
+                  [update-block-op (gen-update-block-op db)]
+                  [boundary-op gen-boundary]]))
+
+(defn- get-db-block-set
+  [db]
+  (set
+   (apply concat
+          (d/q '[:find ?uuid
+                 :where
+                 [?b :block/uuid ?uuid]
+                 [?b :block/parent ?parent]
+                 [?b :block/left ?left]
+                 [?parent :block/uuid ?parent-uuid]
+                 [?left :block/uuid ?left-uuid]]
+               db))))
+
+
+(defn- check-block-count
+  [{:keys [op tx]} current-db]
+  (case (first op)
+    :frontend.worker.undo-redo/move-block
+    (assert (= (:block-origin-left (second op))
+               (:block/uuid (:block/left (d/entity current-db [:block/uuid (:block-uuid (second op))]))))
+            {:op op :tx-data (:tx-data tx) :x (keys tx)})
+
+    :frontend.worker.undo-redo/update-block
+    (assert (some? (d/entity current-db [:block/uuid (:block-uuid (second op))]))
+            {:op op :tx-data (:tx-data tx)})
+
+    :frontend.worker.undo-redo/insert-block
+    (assert (nil? (d/entity current-db [:block/uuid (:block-uuid (second op))]))
+            {:op op :tx-data (:tx-data tx) :x (keys tx)})
+    :frontend.worker.undo-redo/remove-block
+    (assert (some? (d/entity current-db [:block/uuid (:block-uuid (second op))]))
+            {:op op :tx-data (:tx-data tx) :x (keys tx)})
+    ;; else
+    nil))
+
+(defn- undo-all-then-redo-all
+  [conn]
+  (binding [undo-redo/*undo-redo-info-for-test* (atom nil)]
+    (loop [i 0]
+      (let [r (undo-redo/undo test-helper/test-db-name-db-version conn)
+            current-db @conn]
+        (check-block-count @undo-redo/*undo-redo-info-for-test* current-db)
+        (if (not= :frontend.worker.undo-redo/empty-undo-stack r)
+          (recur (inc i))
+          (prn :undo-count i))))
+
+    (loop []
+      (let [r (undo-redo/redo test-helper/test-db-name-db-version conn)
+            current-db @conn]
+        (check-block-count @undo-redo/*undo-redo-info-for-test* current-db)
+        (when (not= :frontend.worker.undo-redo/empty-redo-stack r)
+          (recur))))))
+
+(deftest undo-redo-gen-test
+  (let [conn (db/get-db false)
+        all-remove-ops (gen/generate (gen/vector (gen-op @conn {:remove-block-op 1000}) 20))]
+    (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version all-remove-ops)
+    (prn :block-count-before-init (count (get-db-block-set @conn)))
+    (loop [i 0]
+      (when (not= :frontend.worker.undo-redo/empty-undo-stack
+                  (undo-redo/undo test-helper/test-db-name-db-version conn))
+        (recur (inc i))))
+    (prn :block-count (count (get-db-block-set @conn)))
+    (undo-redo/clear-undo-redo-stack)
+    (testing "move blocks"
+      (let [origin-graph-block-set (get-db-block-set @conn)
+            ops (gen/generate (gen/vector (gen-op @conn {:move-block-op 1000 :boundary-op 500}) 300))]
+        (prn :ops (count ops))
+        (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version ops)
+
+        (undo-all-then-redo-all conn)
+        (undo-all-then-redo-all conn)
+        (undo-all-then-redo-all conn)
+
+        (is (= origin-graph-block-set (get-db-block-set @conn)))))
+
+    (testing "random ops"
+      (let [origin-graph-block-set (get-db-block-set @conn)
+            ops (gen/generate (gen/vector (gen-op @conn) 1000))]
+        (prn :ops (count ops))
+        (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version ops)
+
+        (undo-all-then-redo-all conn)
+        (undo-all-then-redo-all conn)
+        (undo-all-then-redo-all conn)
+
+        (is (= origin-graph-block-set (get-db-block-set @conn)))))
+    ))