Browse Source

Merge branch 'feat/db' into perf/app-start

Tienson Qin 7 months ago
parent
commit
a9e38f0e0d

+ 2 - 9
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -373,12 +373,7 @@
     as there's no chance to introduce timestamps via editing in page
    `skip-existing-page-check?`: if true, allows pages to have the same name"
   [original-page-name db with-timestamp? date-formatter
-   & {:keys [page-uuid class? created-by] :as options}]
-  (assert (or (nil? created-by)
-              (and (map? created-by)
-                   (:block/uuid created-by)
-                   (:logseq.property.user/name created-by)))
-          created-by)
+   & {:keys [page-uuid class?] :as options}]
   (when-not (and db (common-util/uuid-string? original-page-name)
                  (not (ldb/page? (d/entity db [:block/uuid (uuid original-page-name)]))))
     (let [db-based? (ldb/db-based-graph? db)
@@ -404,9 +399,7 @@
           (let [tags (if class? [:logseq.class/Tag]
                          (or (:block/tags page)
                              [:logseq.class/Page]))]
-            (cond-> (assoc page :block/tags tags)
-              created-by
-              (assoc :logseq.property/created-by-ref created-by)))
+            (assoc page :block/tags tags))
           (assoc page :block/type (or (:block/type page) "page")))))))
 
 (defn- db-namespace-page?

+ 71 - 81
deps/outliner/src/logseq/outliner/core.cljs

@@ -52,16 +52,6 @@
   (let [updated-at (common-util/time-ms)]
     (assoc block :block/updated-at updated-at)))
 
-(defn- update-property-created-by
-  [block created-by]
-  (assert (and (map? created-by)
-               (:block/uuid created-by)
-               (:logseq.property.user/name created-by))
-          created-by)
-  (cond-> block
-    (and created-by (nil? (:logseq.property/created-by-ref block)))
-    (assoc :logseq.property/created-by-ref created-by)))
-
 (defn- filter-top-level-blocks
   [db blocks]
   (let [parent-ids (set/intersection (set (map (comp :db/id :block/parent) blocks))
@@ -231,9 +221,9 @@
 
 (extend-type Entity
   otree/INode
-  (-save [this txs-state conn repo _date-formatter {:keys [retract-attributes? retract-attributes]
-                                                    :or {retract-attributes? true}}]
-    (assert (ds/outliner-txs-state? txs-state)
+  (-save [this *txs-state db repo _date-formatter {:keys [retract-attributes? retract-attributes]
+                                                   :or {retract-attributes? true}}]
+    (assert (ds/outliner-txs-state? *txs-state)
             "db should be satisfied outliner-tx-state?")
     (let [data this
           db-based? (sqlite-util/db-based-graph? repo)
@@ -248,8 +238,7 @@
                          :block.temp/ast-title :block.temp/ast-body :block/level :block.temp/fully-loaded?)
                  common-util/remove-nils
                  block-with-updated-at
-                 (fix-tag-ids @conn {:db-graph? db-based?}))
-          db @conn
+                 (fix-tag-ids db {:db-graph? db-based?}))
           db-id (:db/id this)
           block-uuid (:block/uuid this)
           eid (or db-id (when block-uuid [:block/uuid block-uuid]))
@@ -271,9 +260,9 @@
                m*)
           _ (when (and db-based?
                        ;; page or object changed?
-                       (and (or (ldb/page? block-entity) (ldb/object? block-entity))
-                            (:block/title m*)
-                            (not= (:block/title m*) (:block/title block-entity))))
+                       (or (ldb/page? block-entity) (ldb/object? block-entity))
+                       (:block/title m*)
+                       (not= (:block/title m*) (:block/title block-entity)))
               (outliner-validate/validate-block-title db (:block/title m*) block-entity))
           m (cond-> m*
               db-based?
@@ -295,46 +284,46 @@
                                       db-schema/retract-attributes
                                       file-schema/retract-attributes)
                                     retract-attributes)]
-            (swap! txs-state (fn [txs]
-                               (vec
-                                (concat txs
-                                        (map (fn [attribute]
-                                               [:db/retract eid attribute])
-                                             retract-attributes)))))))
+            (swap! *txs-state (fn [txs]
+                                (vec
+                                 (concat txs
+                                         (map (fn [attribute]
+                                                [:db/retract eid attribute])
+                                              retract-attributes)))))))
 
         ;; Update block's page attributes
-        (update-page-when-save-block txs-state block-entity m)
+        (update-page-when-save-block *txs-state block-entity m)
         ;; Remove orphaned refs from block
         (when (and (:block/title m) (not= (:block/title m) (:block/title block-entity)))
-          (remove-orphaned-refs-when-save @conn txs-state block-entity m {:db-graph? db-based?})))
+          (remove-orphaned-refs-when-save db *txs-state block-entity m {:db-graph? db-based?})))
 
       ;; handle others txs
       (let [other-tx (:db/other-tx m)]
         (when (seq other-tx)
-          (swap! txs-state (fn [txs]
-                             (vec (concat txs other-tx)))))
-        (swap! txs-state conj
+          (swap! *txs-state (fn [txs]
+                              (vec (concat txs other-tx)))))
+        (swap! *txs-state conj
                (dissoc m :db/other-tx)))
 
       ;; delete tags when title changed
       (when (and db-based? (:block/tags block-entity) block-entity)
         (let [tx-data (remove-tags-when-title-changed block-entity (:block/title m))]
           (when (seq tx-data)
-            (swap! txs-state (fn [txs] (concat txs tx-data))))))
+            (swap! *txs-state (fn [txs] (concat txs tx-data))))))
 
       this))
 
-  (-del [this txs-state conn]
-    (assert (ds/outliner-txs-state? txs-state)
+  (-del [this *txs-state db]
+    (assert (ds/outliner-txs-state? *txs-state)
             "db should be satisfied outliner-tx-state?")
     (let [block-id (:block/uuid this)
           ids (->>
-               (let [children (ldb/get-block-children @conn block-id)
+               (let [children (ldb/get-block-children db block-id)
                      children-ids (map :block/uuid children)]
                  (conj children-ids block-id))
                (remove nil?))
           txs (map (fn [id] [:db.fn/retractEntity [:block/uuid id]]) ids)
-          page-tx (let [block (d/entity @conn [:block/uuid block-id])]
+          page-tx (let [block (d/entity db [:block/uuid block-id])]
                     (when (:block/pre-block? block)
                       (let [id (:db/id (:block/page block))]
                         [[:db/retract id :block/properties]
@@ -342,7 +331,7 @@
                          [:db/retract id :block/properties-text-values]
                          [:db/retract id :block/alias]
                          [:db/retract id :block/tags]])))]
-      (swap! txs-state concat txs page-tx)
+      (swap! *txs-state concat txs page-tx)
       block-id)))
 
 (defn- assoc-level-aux
@@ -424,19 +413,19 @@
 
 (defn ^:api save-block
   "Save the `block`."
-  [repo conn date-formatter block opts]
+  [repo db date-formatter block opts]
   {:pre [(map? block)]}
-  (let [txs-state (atom [])
+  (let [*txs-state (atom [])
         block' (if (de/entity? block)
                  block
                  (do
                    (assert (or (:db/id block) (:block/uuid block)) "save-block db/id not exists")
                    (when-let [eid (or (:db/id block) (when-let [id (:block/uuid block)] [:block/uuid id]))]
-                     (let [ent (d/entity @conn eid)]
+                     (let [ent (d/entity db eid)]
                        (assert (some? ent) "save-block entity not exists")
                        (merge ent block)))))]
-    (otree/-save block' txs-state conn repo date-formatter opts)
-    {:tx-data @txs-state}))
+    (otree/-save block' *txs-state db repo date-formatter opts)
+    {:tx-data @*txs-state}))
 
 (defn- get-right-siblings
   "Get `node`'s right siblings."
@@ -648,7 +637,7 @@
 (defn ^:api ^:large-vars/cleanup-todo insert-blocks
   "Insert blocks as children (or siblings) of target-node.
   Args:
-    `conn`: db connection.
+    `db`: db
     `blocks`: blocks should be sorted already.
     `target-block`: where `blocks` will be inserted.
     Options:
@@ -661,13 +650,12 @@
       `replace-empty-target?`: If the `target-block` is an empty block, whether
                                to replace it, it defaults to be `false`.
       `update-timestamps?`: whether to update `blocks` timestamps.
-      `created-by`: user-block, update `:logseq.property/created-by-ref` if exists
     ``"
-  [repo conn blocks target-block {:keys [keep-uuid? keep-block-order?
-                                         outliner-op replace-empty-target? update-timestamps?
-                                         created-by insert-template?]
-                                  :as opts
-                                  :or {update-timestamps? true}}]
+  [repo db blocks target-block {:keys [_sibling? keep-uuid? keep-block-order?
+                                       outliner-op replace-empty-target? update-timestamps?
+                                       insert-template?]
+                                :as opts
+                                :or {update-timestamps? true}}]
   {:pre [(seq blocks)
          (m/validate block-map-or-entity target-block)]}
   (let [blocks (keep (fn [b]
@@ -675,7 +663,7 @@
                                         (when-let [id (:block/uuid b)]
                                           [:block/uuid id]))]
                          (->
-                          (if-let [e (if (de/entity? b) b (d/entity @conn eid))]
+                          (if-let [e (if (de/entity? b) b (d/entity db eid))]
                             (merge
                              (into {} e)
                              {:db/id (:db/id e)
@@ -685,7 +673,7 @@
                           (dissoc :block/tx-id :block/refs :block/path-refs))
                          b))
                      blocks)
-        [target-block sibling?] (get-target-block @conn blocks target-block opts)
+        [target-block sibling?] (get-target-block db blocks target-block opts)
         _ (assert (some? target-block) (str "Invalid target: " target-block))
         sibling? (if (ldb/page? target-block) false sibling?)
         replace-empty-target? (if (and (some? replace-empty-target?)
@@ -705,16 +693,14 @@
                         true
                         (mapv block-with-timestamps)
                         db-based?
-                        (mapv #(cond-> %
-                                 true (dissoc :block/properties)
-                                 created-by (update-property-created-by created-by)))))
+                        (mapv #(-> % (dissoc :block/properties)))))
             insert-opts {:sibling? sibling?
                          :replace-empty-target? replace-empty-target?
                          :keep-uuid? keep-uuid?
                          :keep-block-order? keep-block-order?
                          :outliner-op outliner-op
                          :insert-template? insert-template?}
-            {:keys [id->new-uuid blocks-tx]} (insert-blocks-aux @conn blocks' target-block insert-opts)]
+            {:keys [id->new-uuid blocks-tx]} (insert-blocks-aux db blocks' target-block insert-opts)]
         (if (some (fn [b] (or (nil? (:block/parent b)) (nil? (:block/order b)))) blocks-tx)
           (throw (ex-info "Invalid outliner data"
                           {:opts insert-opts
@@ -735,7 +721,7 @@
                                                      :logseq.property/created-from-property (:db/id from-property)}
                                                     [:db/add
                                                      (:db/id (:block/parent target-block))
-                                                     (:db/ident (d/entity @conn (:db/id from-property)))
+                                                     (:db/ident (d/entity db (:db/id from-property)))
                                                      [:block/uuid new-id]]])) top-level-blocks)))
                 full-tx (common-util/concat-without-nil (if (and keep-uuid? replace-empty-target?) (rest uuids-tx) uuids-tx)
                                                         tx
@@ -760,8 +746,8 @@
             page-blocks)))
 
 (defn- delete-block
-  [conn txs-state node]
-  (otree/-del node txs-state conn)
+  [db txs-state node]
+  (otree/-del node txs-state db)
   @txs-state)
 
 (defn- get-top-level-blocks
@@ -775,9 +761,9 @@
 
 (defn ^:api ^:large-vars/cleanup-todo delete-blocks
   "Delete blocks from the tree."
-  [conn blocks]
-  (let [top-level-blocks (filter-top-level-blocks @conn blocks)
-        non-consecutive? (and (> (count top-level-blocks) 1) (seq (ldb/get-non-consecutive-blocks @conn top-level-blocks)))
+  [db blocks]
+  (let [top-level-blocks (filter-top-level-blocks db blocks)
+        non-consecutive? (and (> (count top-level-blocks) 1) (seq (ldb/get-non-consecutive-blocks db top-level-blocks)))
         top-level-blocks* (->> (get-top-level-blocks top-level-blocks non-consecutive?)
                                (remove ldb/page?))
         top-level-blocks (remove :logseq.property/built-in? top-level-blocks*)
@@ -801,18 +787,18 @@
                                          (not (:block/closed-value-property start-block)))]
         (cond
           (and delete-one-block? default-value-property?)
-          (let [datoms (d/datoms @conn :avet (:db/ident from-property) (:db/id start-block))
+          (let [datoms (d/datoms db :avet (:db/ident from-property) (:db/id start-block))
                 tx-data (map (fn [d] {:db/id (:e d)
                                       (:db/ident from-property) :logseq.property/empty-placeholder}) datoms)]
             (when (seq tx-data) (swap! txs-state concat tx-data)))
 
           delete-one-block?
-          (delete-block conn txs-state start-block)
+          (delete-block db txs-state start-block)
 
           :else
           (doseq [id block-ids]
-            (let [node (d/entity @conn id)]
-              (otree/-del node txs-state conn))))))
+            (let [node (d/entity db id)]
+              (otree/-del node txs-state db))))))
     {:tx-data @txs-state}))
 
 (defn- move-to-original-position?
@@ -825,9 +811,8 @@
            (= (:db/id (ldb/get-first-child db (:db/id target-block))) (:db/id block))))))
 
 (defn- move-block
-  [conn block target-block sibling?]
-  (let [db @conn
-        target-block (d/entity db (:db/id target-block))
+  [db block target-block sibling?]
+  (let [target-block (d/entity db (:db/id target-block))
         block (d/entity db (:db/id block))
         first-block-page (:db/id (:block/page block))
         target-page (or (:db/id (:block/page target-block))
@@ -897,7 +882,7 @@
                                      (d/entity @conn (:db/id (nth blocks (dec idx)))))
                     block (d/entity @conn (:db/id block))]
                 (when-not (move-to-original-position? [block] target-block sibling? false)
-                  (let [tx-data (move-block conn block target-block sibling?)]
+                  (let [tx-data (move-block @conn block target-block sibling?)]
                     ;; (prn "==>> move blocks tx:" tx-data)
                     (ldb/transact! conn tx-data {:sibling? sibling?
                                                  :outliner-op (or outliner-op :move-blocks)}))))))
@@ -1019,20 +1004,25 @@
         (ldb/transact! (second args) (:tx-data result) tx-meta)))
     result))
 
-(defn save-block!
-  [repo conn date-formatter block & {:as opts}]
-  (op-transact! save-block repo conn date-formatter block opts))
-
-(defn insert-blocks!
-  [repo conn blocks target-block opts]
-  (op-transact! insert-blocks repo conn blocks target-block (assoc opts :outliner-op :insert-blocks)))
-
-(defn delete-blocks!
-  [repo conn _date-formatter blocks opts]
-  (op-transact! (fn [_repo conn blocks]
-                  (let [{:keys [tx-data]} (#'delete-blocks conn blocks)]
-                    {:tx-data tx-data
-                     :tx-meta (select-keys opts [:outliner-op])})) repo conn blocks opts))
+(let [f (fn [repo conn date-formatter block opts]
+          (save-block repo @conn date-formatter block opts))]
+  (defn save-block!
+    [repo conn date-formatter block & {:as opts}]
+    (op-transact! f repo conn date-formatter block opts)))
+
+(let [f (fn [repo conn blocks target-block opts]
+          (insert-blocks repo @conn blocks target-block opts))]
+  (defn insert-blocks!
+    [repo conn blocks target-block opts]
+    (op-transact! f repo conn blocks target-block (assoc opts :outliner-op :insert-blocks))))
+
+(let [f (fn [_repo conn blocks opts]
+          (let [{:keys [tx-data]} (delete-blocks @conn blocks)]
+            {:tx-data tx-data
+             :tx-meta (select-keys opts [:outliner-op])}))]
+  (defn delete-blocks!
+    [repo conn _date-formatter blocks opts]
+    (op-transact! f repo conn blocks opts)))
 
 (defn move-blocks!
   [repo conn blocks target-block sibling?]

+ 2 - 2
deps/outliner/src/logseq/outliner/property.cljs

@@ -284,7 +284,7 @@
                                               entities)
                            ;; Delete property value block if it's no longer used by other blocks
                            retract-blocks-tx (when (seq deleting-entities)
-                                               (:tx-data (outliner-core/delete-blocks conn deleting-entities)))]
+                                               (:tx-data (outliner-core/delete-blocks @conn deleting-entities)))]
                        (concat
                         [[:db/retract (:db/id block) (:db/ident property)]]
                         retract-blocks-tx)))
@@ -627,7 +627,7 @@
                       {:type :notification
                        :payload {:message "The choice can't be deleted because it's built-in."
                                  :type :warning}}))
-      (let [data (:tx-data (outliner-core/delete-blocks conn [value-block]))
+      (let [data (:tx-data (outliner-core/delete-blocks @conn [value-block]))
             tx-data (conj data (outliner-core/block-with-updated-at
                                 {:db/id property-id}))]
         (ldb/transact! conn tx-data)))))

+ 2 - 2
deps/outliner/src/logseq/outliner/tree.cljs

@@ -6,8 +6,8 @@
             [logseq.db.common.property-util :as db-property-util]))
 
 (defprotocol INode
-  (-save [this txs-state conn repo date-formatter opts])
-  (-del [this db conn]))
+  (-save [this *txs-state conn repo date-formatter opts])
+  (-del [this *txs-state db]))
 
 (defn- blocks->vec-tree-aux
   [repo db blocks root]

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

@@ -472,7 +472,8 @@
                    (util/electron?)
                    (mobile-util/native-platform?))
                (nil? @src))
-      (p/then (assets-handler/<make-asset-url href) #(reset! src %)))
+      (p/then (assets-handler/<make-asset-url href)
+              #(reset! src (common-util/safe-decode-uri-component %))))
 
     (when @src
       (let [ext (keyword (or (util/get-file-ext @src)
@@ -523,6 +524,7 @@
           (= ext :pdf)
           [:a.asset-ref.is-pdf
            {:data-href href
+            :data-url @src
             :draggable true
             :on-drag-start #(.setData (gobj/get % "dataTransfer") "file" href)
             :on-click (fn [e]
@@ -2101,8 +2103,10 @@
   (string/blank? (:block/title block)))
 
 (rum/defcs ^:large-vars/cleanup-todo block-control < rum/reactive
+  (rum/local false ::dragging?)
   [state config block {:keys [uuid block-id collapsed? *control-show? edit? selected? top? bottom?]}]
-  (let [doc-mode?          (state/sub :document/mode?)
+  (let [*dragging?         (::dragging? state)
+        doc-mode?          (state/sub :document/mode?)
         control-show?      (util/react *control-show?)
         ref?               (:ref? config)
         empty-content?     (block-content-empty? block)
@@ -2152,8 +2156,11 @@
                       {:id (str "dot-" uuid)
                        :draggable true
                        :on-drag-start (fn [event]
+                                        (reset! *dragging? true)
                                         (util/stop-propagation event)
                                         (bullet-drag-start event block uuid block-id))
+                       :on-drag-end (fn [_]
+                                      (reset! *dragging? false))
                        :blockid (str uuid)
                        :class (str (when collapsed? "bullet-closed")
                                    (when (and (:document/mode? config)
@@ -2193,15 +2200,15 @@
 
                        :else
                        bullet)]
-         (if (config/db-based-graph?)
-           (ui/tippy
-            {:html (fn []
-                     [:div.flex.flex-col.gap-1.p-2
-                      (when-let [created-by (and (ldb/get-graph-rtc-uuid (db/get-db)) (:logseq.property/created-by-ref block))]
-                        [:div (:block/title created-by)])
-                      [:div "Created: " (date/int->local-time-2 (:block/created-at block))]
-                      [:div "Last edited: " (date/int->local-time-2 (:block/updated-at block))]])}
-            bullet')
+         (if (and (config/db-based-graph?) (not @*dragging?))
+           (ui/tooltip
+            bullet'
+            [:div.flex.flex-col.gap-1.p-2
+             (when-let [created-by (and (ldb/get-graph-rtc-uuid (db/get-db))
+                                        (:logseq.property/created-by-ref block))]
+               [:div (:block/title created-by)])
+             [:div "Created: " (date/int->local-time-2 (:block/created-at block))]
+             [:div "Last edited: " (date/int->local-time-2 (:block/updated-at block))]])
            bullet')))]))
 
 (rum/defc dnd-separator
@@ -3181,7 +3188,7 @@
                                  [block
                                   (when ast-title
                                     (if (seq ast-title)
-                                      (->elem :span.inline-wrap (map-inline config ast-title))
+                                      (->elem :span (map-inline config ast-title))
                                       (->elem :div (markup-elements-cp config ast-body))))]))))
             breadcrumbs (->> (into [] parents-props)
                              (concat [page-name-props] (when more? [:more]))

+ 9 - 5
src/main/frontend/components/block.css

@@ -101,17 +101,15 @@
 }
 
 .breadcrumb {
-  @apply flex flex-row items-center flex-wrap;
+  @apply inline;
 
   .asset-container > img {
     height: 18px;
     width: unset !important;
   }
 
-  .inline-wrap {
-    & > div, & > div > div {
-      display: inherit;
-    }
+  a, svg, div {
+    @apply inline;
   }
 
   &.block-parents {
@@ -410,6 +408,12 @@
       @apply inline;
     }
   }
+
+  .block-title-wrap {
+    > .prefix-link {
+      @apply inline mx-1;
+    }
+  }
 }
 
 .asset-ref {

+ 2 - 2
src/main/frontend/components/page.cljs

@@ -91,8 +91,7 @@
   [page-e blocks config sidebar? whiteboard? _block-uuid]
   (when page-e
     (let [hiccup (component-block/->hiccup blocks config {})]
-      [:div.page-blocks-inner {:style {:margin-left (if whiteboard? 0 -20)
-                                       :min-height 29}}
+      [:div.page-blocks-inner {:style {:min-height 29}}
        (rum/with-key
          (content/content (str (:block/uuid page-e))
                           {:hiccup   hiccup
@@ -677,6 +676,7 @@
 
             (when (or (not show-tabs?) tabs-rendered?)
               [:div.ls-page-blocks
+               {:style {:margin-left (if whiteboard? 0 -20)}}
                (page-blocks-cp page (merge option {:sidebar? sidebar?
                                                    :container-id (:container-id state)
                                                    :whiteboard? whiteboard?}))])])

+ 7 - 1
src/main/frontend/extensions/pdf/pdf.css

@@ -1009,5 +1009,11 @@ html.is-system-window {
 }
 
 .pdfViewer .page.loadingIcon::after {
-  background: none;
+  @apply bg-none;
 }
+
+.textLayer :is(span, br) {
+  &::selection {
+    @apply text-transparent;
+  }
+}

+ 1 - 0
src/main/frontend/handler/assets.cljs

@@ -178,6 +178,7 @@
         (resolve-asset-real-path-url (state/get-current-repo) path)
 
         (util/electron?)
+        ;; fullpath will be encoded
         (path/prepend-protocol "assets:" full-path)
 
         (mobile-util/native-platform?)

+ 1 - 4
src/main/frontend/handler/common/page.cljs

@@ -14,7 +14,6 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
-            [frontend.handler.user :as user]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.state :as state]
@@ -63,9 +62,7 @@
            (p/let [options' (if db-based?
                               (cond-> (update options :tags concat (:block/tags parsed-result))
                                 (nil? (:split-namespace? options))
-                                (assoc :split-namespace? true)
-                                true
-                                (assoc :created-by (user/user-block)))
+                                (assoc :split-namespace? true))
                               options)
                    [_page-name page-uuid] (ui-outliner-tx/transact!
                                            {:outliner-op :create-page}

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

@@ -29,7 +29,6 @@
             [frontend.handler.property.file :as property-file]
             [frontend.handler.property.util :as pu]
             [frontend.handler.route :as route-handler]
-            [frontend.handler.user :as user]
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.tree :as tree]
@@ -342,8 +341,7 @@
      (outliner-op/insert-blocks! [new-block] current-block {:sibling? sibling?
                                                             :keep-uuid? keep-uuid?
                                                             :ordered-list? ordered-list?
-                                                            :replace-empty-target? replace-empty-target?
-                                                            :created-by (user/user-block)}))))
+                                                            :replace-empty-target? replace-empty-target?}))))
 
 (defn- block-self-alone-when-insert?
   [config uuid]

+ 8 - 17
src/main/frontend/handler/user.cljs

@@ -85,19 +85,6 @@
    parse-jwt
    :sub))
 
-(defn user-block
-  "FIXME: move to somewhere else?"
-  []
-  (when-let [user-uuid* (user-uuid)]
-    (let [user-name (username)
-          email* (email)]
-      {:block/uuid (uuid user-uuid*)
-       :block/name user-name
-       :block/title user-name
-       :block/tags :logseq.class/Page
-       :logseq.property.user/name user-name
-       :logseq.property.user/email email*})))
-
 (defn logged-in? []
   (some? (state/get-auth-refresh-token)))
 
@@ -126,7 +113,8 @@
    (state/set-auth-access-token nil)
    (state/set-auth-refresh-token nil)
    (set-token-to-localstorage! "" "" "")
-   (clear-cognito-tokens!))
+   (clear-cognito-tokens!)
+   (state/<invoke-db-worker :thread-api/update-auth-tokens nil nil nil))
   ([except-refresh-token?]
    (state/set-auth-id-token nil)
    (state/set-auth-access-token nil)
@@ -134,18 +122,21 @@
      (state/set-auth-refresh-token nil))
    (if except-refresh-token?
      (set-token-to-localstorage! "" "")
-     (set-token-to-localstorage! "" "" ""))))
+     (set-token-to-localstorage! "" "" ""))
+   (state/<invoke-db-worker :thread-api/update-auth-tokens nil nil (state/get-auth-refresh-token))))
 
 (defn- set-tokens!
   ([id-token access-token]
    (state/set-auth-id-token id-token)
    (state/set-auth-access-token access-token)
-   (set-token-to-localstorage! id-token access-token))
+   (set-token-to-localstorage! id-token access-token)
+   (state/<invoke-db-worker :thread-api/update-auth-tokens id-token access-token (state/get-auth-refresh-token)))
   ([id-token access-token refresh-token]
    (state/set-auth-id-token id-token)
    (state/set-auth-access-token access-token)
    (state/set-auth-refresh-token refresh-token)
-   (set-token-to-localstorage! id-token access-token refresh-token)))
+   (set-token-to-localstorage! id-token access-token refresh-token)
+   (state/<invoke-db-worker :thread-api/update-auth-tokens id-token access-token refresh-token)))
 
 (defn- <refresh-tokens
   "return refreshed id-token, access-token"

+ 7 - 2
src/main/frontend/worker/db_worker.cljs

@@ -24,8 +24,8 @@
             [frontend.worker.rtc.core]
             [frontend.worker.rtc.db-listener]
             [frontend.worker.search :as search]
-            [frontend.worker.state :as worker-state] ;; [frontend.worker.undo-redo :as undo-redo]
-            [frontend.worker.undo-redo2 :as undo-redo]
+            [frontend.worker.state :as worker-state]
+            [frontend.worker.undo-redo :as undo-redo]
             [frontend.worker.util :as worker-util]
             [goog.object :as gobj]
             [lambdaisland.glogi.console :as glogi-console]
@@ -795,6 +795,11 @@
   [repo]
   (get-all-page-titles-with-cache repo))
 
+(def-thread-api :thread-api/update-auth-tokens
+  [id-token access-token refresh-token]
+  (worker-state/set-auth-tokens! id-token access-token refresh-token)
+  nil)
+
 (comment
   (def-thread-api :general/dangerousRemoveAllDbs
     []

+ 0 - 1
src/main/frontend/worker/handler/page.cljs

@@ -34,7 +34,6 @@
    * :tags                     - tag uuids that are added to :block/tags
    * :persist-op?              - when true, add an update-page op
    * :properties               - properties to add to the page
-   * :created-by               - user-block, when set, set :logseq.property/created-by-ref, only for db-based-graphs
   TODO: Add other options"
   [repo conn config title & {:as options}]
   (if (ldb/db-based-graph? @conn)

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

@@ -169,8 +169,7 @@
   "Pure function without side effects"
   [db title*
    {:keys [create-first-block? properties uuid persist-op? whiteboard?
-           class? today-journal? split-namespace? skip-existing-page-check?
-           created-by]
+           class? today-journal? split-namespace? skip-existing-page-check?]
     :or   {create-first-block?      true
            properties               nil
            uuid                     nil
@@ -205,8 +204,7 @@
                                                 :page-uuid (when (uuid? uuid) uuid)
                                                 :skip-existing-page-check? (if (some? skip-existing-page-check?)
                                                                              skip-existing-page-check?
-                                                                             true)
-                                                :created-by created-by})
+                                                                             true)})
             [page parents] (if (and (text/namespace-page? title) split-namespace?)
                              (let [pages (split-namespace-pages db page date-formatter)]
                                [(last pages) (butlast pages)])

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

@@ -7,6 +7,7 @@
             [frontend.worker.state :as worker-state]
             [frontend.worker.util :as worker-util]
             [logseq.common.defkeywords :refer [defkeywords]]
+            [logseq.common.util :as common-util]
             [logseq.common.uuid :as common-uuid]
             [logseq.db :as ldb]
             [logseq.db.frontend.validate :as db-validate]
@@ -48,7 +49,7 @@
             blocks)))
 
 (defn- insert-tag-templates
-  [repo conn tx-report]
+  [repo tx-report]
   (let [db (:db-after tx-report)
         journal-id (:db/id (d/entity db :logseq.class/Journal))
         journal-template? (some (fn [d] (and (:added d) (= (:a d) :block/tags) (= (:v d) journal-id))) (:tx-data tx-report))
@@ -83,8 +84,10 @@
                                                                                                                                               (:block/uuid e)))))))]
                                                                           blocks))))]
                                      (when (seq template-blocks)
-                                       (let [result (outliner-core/insert-blocks repo conn template-blocks object {:sibling? false
-                                                                                                                   :keep-uuid? journal-template?})]
+                                       (let [result (outliner-core/insert-blocks
+                                                     repo db template-blocks object
+                                                     {:sibling? false
+                                                      :keep-uuid? journal-template?})]
                                          (:tx-data result)))))))]
     tx-data))
 
@@ -157,15 +160,57 @@
                                    :db-before (:db-before tx-report)))]
     {:tx-report final-tx-report}))
 
-(defn- invoke-hooks-default [repo conn {:keys [tx-meta] :as tx-report} context]
+(defn- gen-created-by-block
+  [decoded-id-token]
+  (let [user-uuid (:sub decoded-id-token)
+        user-name (:cognito:username decoded-id-token)
+        email (:email decoded-id-token)
+        now (common-util/time-ms)]
+    {:block/uuid (uuid user-uuid)
+     :block/name user-name
+     :block/title user-name
+     :block/tags :logseq.class/Page
+     :block/created-at now
+     :block/updated-at now
+     :logseq.property.user/name user-name
+     :logseq.property.user/email email}))
+
+(defn- add-created-by-ref-hook
+  [db-after tx-data tx-meta]
+  (when (and (not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta)))
+             (seq tx-data))
+    (when-let [decoded-id-token (some-> (worker-state/get-id-token) worker-util/parse-jwt)]
+      (let [created-by-ent (d/entity db-after [:block/uuid (uuid (:sub decoded-id-token))])
+            created-by-block (when (nil? created-by-ent)
+                               (assoc (gen-created-by-block decoded-id-token) :db/id "created-by-id"))
+            created-by-id (or (:db/id created-by-ent) "created-by-id")
+            add-created-by-tx-data
+            (keep
+             (fn [datom]
+               (when (and (keyword-identical? :block/uuid (:a datom))
+                          (:added datom))
+                 (let [e (:e datom)
+                       ent (d/entity db-after e)]
+                   (when-not (:logseq.property/created-by-ref ent)
+                     [:db/add e :logseq.property/created-by-ref created-by-id]))))
+             tx-data)]
+        (cond->> add-created-by-tx-data
+          (nil? created-by-ent) (cons created-by-block))))))
+
+(defn- compute-extra-tx-data
+  [repo tx-report]
+  (let [{:keys [db-after tx-data tx-meta]} tx-report
+        display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data)
+        commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
+                      (commands/run-commands tx-report))
+        insert-templates-tx (insert-tag-templates repo tx-report)
+        created-by-tx (add-created-by-ref-hook db-after tx-data tx-meta)]
+    (concat display-blocks-tx-data commands-tx insert-templates-tx created-by-tx)))
+
+(defn- invoke-hooks-default
+  [repo conn {:keys [tx-meta] :as tx-report} context]
   (try
-    (let [display-blocks-tx-data (add-missing-properties-to-typed-display-blocks (:db-after tx-report) (:tx-data tx-report))
-          commands-tx (when-not (or (:undo? tx-meta) (:redo? tx-meta) (:rtc-tx? tx-meta))
-                        (commands/run-commands tx-report))
-          ;; :block/refs relies on those changes
-          ;; idea: implement insert-templates using a command?
-          insert-templates-tx (insert-tag-templates repo conn tx-report)
-          tx-before-refs (concat display-blocks-tx-data commands-tx insert-templates-tx)
+    (let [tx-before-refs (compute-extra-tx-data repo tx-report)
           tx-report* (if (seq tx-before-refs)
                        (let [result (ldb/transact! conn tx-before-refs {:pipeline-replace? true
                                                                         :outliner-op :pre-hook-invoke})]
@@ -191,15 +236,15 @@
           block-refs (when (seq blocks')
                        (rebuild-block-refs repo tx-report* blocks'))
           refs-tx-report (when (seq block-refs)
-                           (ldb/transact! conn (concat insert-templates-tx block-refs) {:pipeline-replace? true}))
+                           (ldb/transact! conn block-refs {:pipeline-replace? true}))
           replace-tx (let [db-after (or (:db-after refs-tx-report) (:db-after tx-report*))]
                        (concat
-                      ;; block path refs
+                        ;; block path refs
                         (when (seq blocks')
                           (let [blocks' (keep (fn [b] (d/entity db-after (:db/id b))) blocks')]
                             (compute-block-path-refs-tx tx-report* blocks')))
 
-                       ;; update block/tx-id
+                        ;; update block/tx-id
                         (let [updated-blocks (remove (fn [b] (contains? deleted-block-ids (:db/id b)))
                                                      (concat pages blocks))
                               tx-id (get-in (or refs-tx-report tx-report*) [:tempids :db/current-tx])]

+ 40 - 18
src/main/frontend/worker/rtc/full_upload_download_graph.cljs

@@ -287,7 +287,7 @@
     (transact-block-refs! repo)))
 
 (defn- blocks-resolve-temp-id
-  [blocks]
+  [schema-blocks blocks]
   (let [uuids (map :block/uuid blocks)
         idents (map :db/ident blocks)
         ids (map :db/id blocks)
@@ -300,10 +300,16 @@
                               ident
                               (assoc :db/ident ident)))) ids)
         id-ref-exists? (fn [v] (and (string? v) (or (get id->ident v) (get id->uuid v))))
+        ref-k-set (set (keep (fn [b] (when (= :db.type/ref (:db/valueType b))
+                                       (:db/ident b)))
+                             schema-blocks))
+        ref-k? (fn [k] (contains? ref-k-set k))
         blocks-tx-data (map (fn [block]
                               (->> (map
                                     (fn [[k v]]
-                                      (let [v (cond
+                                      (let [v
+                                            (if (ref-k? k)
+                                              (cond
                                                 (id-ref-exists? v)
                                                 (or (get id->ident v) [:block/uuid (get id->uuid v)])
 
@@ -311,15 +317,16 @@
                                                 (map (fn [id] (or (get id->ident id) [:block/uuid (get id->uuid id)])) v)
 
                                                 :else
-                                                v)]
+                                                v)
+                                              v)]
                                         [k v]))
                                     (dissoc block :db/id))
                                    (into {}))) blocks)]
     (concat id-tx-data blocks-tx-data)))
 
-(defn- remote-all-blocks=>client-blocks+t
+(defn- remote-all-blocks=>client-blocks
   [all-blocks ignore-attr-set ignore-entity-set]
-  (let [{:keys [t blocks]} all-blocks
+  (let [{:keys [_ t blocks]} all-blocks
         card-one-attrs (blocks->card-one-attrs blocks)
         blocks1 (worker-util/profile :convert-card-one-value-from-value-coll
                                      (map (partial convert-card-one-value-from-value-coll card-one-attrs) blocks))
@@ -336,23 +343,38 @@
                         (into {} (remove (comp (partial contains? ignore-attr-set) first)) block))))
                 blocks2)
         blocks (fill-block-fields blocks)]
-    {:blocks blocks :t t}))
+    blocks))
 
-(defn- new-task--transact-remote-all-blocks
-  [all-blocks repo graph-uuid]
-  (let [{:keys [t blocks]} (remote-all-blocks=>client-blocks+t
-                            all-blocks
-                            rtc-const/ignore-attrs-when-init-download
-                            rtc-const/ignore-entities-when-init-download)
+(defn- remote-all-blocks->tx-data+t
+  "Return
+  {:remote-t ...
+   :init-tx-data ...
+   :tx-data ...}
+  init-tx-data - schema data and other init-data, need to be transacted first
+  tx-data - all other data"
+  [remote-all-blocks graph-uuid]
+  (let [t (:t remote-all-blocks)
+        blocks (remote-all-blocks=>client-blocks
+                remote-all-blocks
+                rtc-const/ignore-attrs-when-init-download
+                rtc-const/ignore-entities-when-init-download)
         [schema-blocks normal-blocks] (blocks->schema-blocks+normal-blocks blocks)
         tx-data (concat
-                 (blocks-resolve-temp-id normal-blocks)
+                 (blocks-resolve-temp-id schema-blocks normal-blocks)
                  [(ldb/kv :logseq.kv/graph-uuid graph-uuid)])
         init-tx-data (cons (ldb/kv :logseq.kv/db-type "db") schema-blocks)]
+    {:remote-t t
+     :init-tx-data init-tx-data
+     :tx-data tx-data}))
+
+(defn- new-task--transact-remote-all-blocks!
+  [all-blocks repo graph-uuid]
+  (let [{:keys [remote-t init-tx-data tx-data]}
+        (remote-all-blocks->tx-data+t all-blocks graph-uuid)]
     (m/sp
-      (client-op/update-local-tx repo t)
-      (rtc-log-and-state/update-local-t graph-uuid t)
-      (rtc-log-and-state/update-remote-t graph-uuid t)
+      (client-op/update-local-tx repo remote-t)
+      (rtc-log-and-state/update-local-t graph-uuid remote-t)
+      (rtc-log-and-state/update-remote-t graph-uuid remote-t)
       (if rtc-const/RTC-E2E-TEST
         (create-graph-for-rtc-test repo init-tx-data tx-data)
         (c.m/<?
@@ -363,7 +385,7 @@
            repo init-tx-data
            {:rtc-download-graph? true
             :gen-undo-ops? false
-            ;; only transact db schema, skip validation to avoid warning
+             ;; only transact db schema, skip validation to avoid warning
             :frontend.worker.pipeline/skip-validate-db? true
             :persist-op? false}
            (worker-state/get-context))
@@ -437,7 +459,7 @@
                                                         :graph-uuid graph-uuid})
           (let [all-blocks (ldb/read-transit-str body)]
             (worker-state/set-rtc-downloading-graph! true)
-            (m/? (new-task--transact-remote-all-blocks all-blocks repo graph-uuid))
+            (m/? (new-task--transact-remote-all-blocks! all-blocks repo graph-uuid))
             (client-op/update-graph-uuid repo graph-uuid)
             (when-not rtc-const/RTC-E2E-TEST
               (c.m/<? (worker-db-metadata/<store repo (pr-str {:kv/value graph-uuid}))))

+ 15 - 0
src/main/frontend/worker/state.cljs

@@ -37,6 +37,10 @@
                        :config {}
                        :git/current-repo nil
 
+                       :auth/id-token nil
+                       :auth/access-token nil
+                       :auth/refresh-token nil
+
                        :rtc/downloading-graph? false
 
                        :undo/repo->page-block-uuid->undo-ops (atom {})
@@ -127,3 +131,14 @@
 (defn set-rtc-downloading-graph!
   [value]
   (swap! *state assoc :rtc/downloading-graph? value))
+
+(defn set-auth-tokens!
+  [id-token access-token refresh-token]
+  (swap! *state assoc
+         :auth/id-token id-token
+         :auth/access-token access-token
+         :auth/refresh-token refresh-token))
+
+(defn get-id-token
+  []
+  (:auth/id-token @*state))

+ 302 - 552
src/main/frontend/worker/undo_redo.cljs

@@ -1,590 +1,340 @@
 (ns frontend.worker.undo-redo
-  "undo/redo related fns and op-schema"
+  "Undo redo new implementation"
   (:require [clojure.set :as set]
             [datascript.core :as d]
             [frontend.worker.db-listener :as db-listener]
             [frontend.worker.state :as worker-state]
-            [logseq.common.config :as common-config]
-            [logseq.common.util :as common-util]
+            [logseq.common.defkeywords :refer [defkeywords]]
             [logseq.db :as ldb]
-            [logseq.outliner.batch-tx :include-macros true :as batch-tx]
-            [logseq.outliner.core :as outliner-core]
-            [logseq.outliner.transaction :as outliner-tx]
             [malli.core :as m]
             [malli.util :as mu]))
-(comment
-  ;; this ns is not used currently, so just comment out these kw definitions
-  ;; use logseq.common.defkeywords/defkeywords instead
-
-  (sr/defkeyword :gen-undo-ops?
-    "tx-meta option, generate undo ops from tx-data when true (default true)")
-
-  (sr/defkeyword :gen-undo-boundary-op?
-    "tx-meta option, generate `::boundary` undo-op when true (default true).
-usually every transaction's tx-data will generate ops like: [<boundary> <op1> <op2> ...],
-push to undo-stack, result in [...<boundary> <op0> <boundary> <op1> <op2> ...].
-
-when this option is false, only generate [<op1> <op2> ...]. undo-stack: [...<boundary> <op0> <op1> <op2> ...]
-so when undo, it will undo [<op0> <op1> <op2>] instead of [<op1> <op2>]")
-
-  (sr/defkeyword ::boundary
-    "boundary of one or more undo-ops.
-when one undo/redo will operate on all ops between two ::boundary")
-
-  (sr/defkeyword ::insert-blocks
-    "when some blocks are inserted, generate a ::insert-blocks undo-op.
-when undo this op, the related blocks will be removed.")
-
-  (sr/defkeyword ::move-block
-    "when a block is moved, generate a ::move-block undo-op.")
-
-  (sr/defkeyword ::remove-block
-    "when a block is removed, generate a ::remove-block undo-op.
-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 ::record-editor-info
-    "record current editor and cursor")
 
-  (sr/defkeyword ::empty-undo-stack
-    "return by undo, when no more undo ops")
+(defkeywords
+  ::record-editor-info {:doc "record current editor and cursor"}
+  ::db-transact {:doc "db tx"}
+  ::ui-state {:doc "ui state such as route && sidebar blocks"})
 
-  (sr/defkeyword ::empty-redo-stack
-    "return by redo, when no more redo ops"))
-
-(def ^:private boundary [::boundary])
+;; TODO: add other UI states such as `::ui-updates`.
+(comment
+  ;; TODO: convert it to a qualified-keyword
+  (sr/defkeyword :gen-undo-ops?
+    "tx-meta option, generate undo ops from tx-data when true (default true)"))
 
-(def ^:private undo-op-schema
+(def ^:private undo-op-item-schema
   (mu/closed-schema
    [:multi {:dispatch first}
-    [::boundary
-     [:cat :keyword]]
-    [::insert-blocks
-     [:cat :keyword
-      [:map
-       [:block-uuids [:sequential :uuid]]]]]
-    [::move-block
+    [::db-transact
      [:cat :keyword
       [:map
-       [:block-uuid :uuid]
-       [:block-origin-left :uuid]
-       [:block-origin-parent :uuid]]]]
-    [::remove-block
-     [:cat :keyword
-      [:map
-       [:block-uuid :uuid]
-       [:block-entity-map
-        [:map
-         [:block/uuid :uuid]
-         [:block/left :uuid]
-         [:block/parent :uuid]
-         [:block/title :string]
-         [:block/created-at {:optional true} :int]
-         [:block/updated-at {:optional true} :int]
-         [:block/format {:optional true} :any]
-         [:block/tags {:optional true} [:sequential :uuid]]
-         [:block/link {:optional true} [:maybe :uuid]]]]]]]
-    [::update-block
-     [:cat :keyword
-      [:map
-       [:block-uuid :uuid]
-       [:block-origin-content {:optional true} :string]
-       [:block-origin-tags {:optional true} [:sequential :uuid]]
-       [:block-origin-collapsed {:optional true} :boolean]
-       [:block-origin-link {:optional true} [:maybe :uuid]]
-       ;; TODO: add more attrs
-       ]]]
+       [:tx-data [:sequential [:fn
+                               {:error/message "should be a Datom"}
+                               d/datom?]]]
+       [:tx-meta [:map {:closed false}
+                  [:outliner-op :keyword]]]
+       [:added-ids [:set :int]]
+       [:retracted-ids [:set :int]]]]]
+
     [::record-editor-info
      [:cat :keyword
       [:map
        [:block-uuid :uuid]
        [:container-id [:or :int [:enum :unknown-container]]]
        [:start-pos [:maybe :int]]
-       [:end-pos [:maybe :int]]]]]]))
-
-(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]}
-   {:block/parent [:block/uuid]}
-   :block/title
-   :block/created-at
-   :block/updated-at
-   :block/format
-   {:block/tags [:block/uuid]}
-   {:block/link [:block/uuid]}])
-
-(defn- ->block-entity-map
-  [db eid]
-  (assert (some? eid))
-  (let [m (d/pull db entity-map-pull-pattern eid)]
-    (cond-> m
-      true                  (update :block/left :block/uuid)
-      true                  (update :block/parent :block/uuid)
-      (seq (:block/tags m)) (update :block/tags (partial mapv :block/uuid))
-      (:block/link m)       (update :block/link :block/uuid))))
-
-(defn- reverse-op
-  "return ops"
-  [db op]
-  (let [block-uuid (:block-uuid (second op))]
-    (case (first op)
-      ::boundary [op]
-
-      ::record-editor-info [op]
-
-      ::insert-blocks
-      (keep
-       (fn [block-uuid]
-         [::remove-block
-          {:block-uuid       block-uuid
-           :block-entity-map (->block-entity-map db [:block/uuid block-uuid])}])
-       (:block-uuids (second op)))
-
-      ::move-block
-      (let [b (d/entity db [:block/uuid block-uuid])]
-        [[::move-block
-          {:block-uuid          block-uuid
-           :block-origin-left   (:block/uuid (:block/left b))
-           :block-origin-parent (:block/uuid (:block/parent b))}]])
-
-      ::remove-block
-      [[::insert-blocks {:block-uuids [block-uuid]}]]
-
-      ::update-block
-      (let [value-keys             (set (keys (second op)))
-            block-entity           (d/entity db [:block/uuid block-uuid])
-            block-origin-content   (when (contains? value-keys :block-origin-content)
-                                     (:block/title block-entity))
-            block-origin-tags      (when (contains? value-keys :block-origin-tags)
-                                     (mapv :block/uuid (:block/tags block-entity)))
-            block-origin-collapsed (when (contains? value-keys :block-origin-collapsed)
-                                     (boolean (:block/collapsed? block-entity)))
-            block-origin-link      (when (contains? value-keys :block-origin-link)
-                                     (:block/uuid (:block/link block-entity)))]
-        [[::update-block
-          (cond-> {:block-uuid block-uuid}
-            (some? block-origin-content)              (assoc :block-origin-content block-origin-content)
-            (some? block-origin-tags)                 (assoc :block-origin-tags block-origin-tags)
-            (some? block-origin-collapsed)            (assoc :block-origin-collapsed block-origin-collapsed)
-            ;; block-origin-link's value maybe nil, so use contains as cond
-            (contains? value-keys :block-origin-link) (assoc :block-origin-link block-origin-link))]])
-      nil)))
-
-(def ^:private apply-conj-vec (partial apply (fnil conj [])))
+       [:end-pos [:maybe :int]]]]]
 
-(comment
-  (def ^:private op-count-hard-limit 3000)
-  (def ^:private op-count-limit 2000))
-
-(defn- push-undo-ops
-  [repo page-block-uuid ops]
-  (assert (and (undo-ops-validator ops)
-               (uuid? page-block-uuid))
-          {:ops ops :page-block-uuid page-block-uuid})
-  (swap! (:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
-         update-in [repo page-block-uuid]
-         apply-conj-vec ops))
-
-(defn- pop-ops-helper
+    [::ui-state
+     [:cat :keyword :string]]]))
+
+(def ^:private undo-op-validator (m/validator [:sequential undo-op-item-schema]))
+
+(defonce max-stack-length 100)
+(defonce *undo-ops (:undo/repo->ops @worker-state/*state))
+(defonce *redo-ops (:redo/repo->ops @worker-state/*state))
+
+(defn- conj-op
+  [col op]
+  (let [result (conj (if (empty? col) [] col) op)]
+    (if (>= (count result) max-stack-length)
+      (subvec result 0 (/ max-stack-length 2))
+      result)))
+
+(defn- pop-stack
   [stack]
-  (let [[ops i]
-        (loop [i (dec (count stack)) r []]
-          (let [peek-op (nth stack i nil)]
-            (cond
-              (neg? i)
-              [r 0]
+  (when (seq stack)
+    [(last stack) (pop stack)]))
 
-              (nil? peek-op)
-              [r i]
+(defn- push-undo-op
+  [repo op]
+  (assert (undo-op-validator op) {:op op})
+  (swap! *undo-ops update repo conj-op op))
 
-              (= boundary peek-op)
-              [r i]
+(defn- push-redo-op
+  [repo op]
+  (assert (undo-op-validator op) {:op op})
+  (swap! *redo-ops update repo conj-op op))
 
-              :else
-              (recur (dec i) (conj r peek-op)))))]
-    [ops (subvec (vec stack) 0 i)]))
-
-(defn- pop-undo-ops
-  [repo page-block-uuid]
-  (assert (uuid? page-block-uuid) page-block-uuid)
-  (let [repo->page-block-uuid->undo-ops (:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
-        undo-stack (get-in @repo->page-block-uuid->undo-ops [repo page-block-uuid])
-        [ops undo-stack*] (pop-ops-helper undo-stack)]
-    (swap! repo->page-block-uuid->undo-ops assoc-in [repo page-block-uuid] undo-stack*)
-    ops))
+(comment
+  ;; This version checks updated datoms by other clients, allows undo and redo back
+  ;; to the current state.
+  ;; The downside is that it'll undo the changes made by others.
+  (defn- pop-undo-op
+    [repo conn]
+    (let [undo-stack (get @*undo-ops repo)
+          [op undo-stack*] (pop-stack undo-stack)]
+      (swap! *undo-ops assoc repo undo-stack*)
+      (mapv (fn [item]
+              (if (= (first item) ::db-transact)
+                (let [m (second item)
+                      tx-data' (mapv
+                                (fn [{:keys [e a v tx add] :as datom}]
+                                  (let [one-value? (= :db.cardinality/one (:db/cardinality (d/entity @conn a)))
+                                        new-value (when (and one-value? add) (get (d/entity @conn e) a))
+                                        value-not-matched? (and (some? new-value) (not= v new-value))]
+                                    (if value-not-matched?
+                                    ;; another client might updated `new-value`, the datom below will be used
+                                    ;; to restore the the current state when redo this undo.
+                                      (d/datom e a new-value tx add)
+                                      datom)))
+                                (:tx-data m))]
+                  [::db-transact (assoc m :tx-data tx-data')])
+                item))
+            op))))
+
+(defn- pop-undo-op
+  [repo]
+  (let [undo-stack (get @*undo-ops repo)
+        [op undo-stack*] (pop-stack undo-stack)]
+    (swap! *undo-ops assoc repo undo-stack*)
+    (let [op' (mapv (fn [item]
+                      (if (= (first item) ::db-transact)
+                        (let [m (second item)
+                              tx-data' (vec (:tx-data m))]
+                          (if (seq tx-data')
+                            [::db-transact (assoc m :tx-data tx-data')]
+                            ::db-transact-no-tx-data))
+                        item))
+                    op)]
+      (when-not (some #{::db-transact-no-tx-data} op')
+        op'))))
+
+(defn- pop-redo-op
+  [repo]
+  (let [redo-stack (get @*redo-ops repo)
+        [op redo-stack*] (pop-stack redo-stack)]
+    (swap! *redo-ops assoc repo redo-stack*)
+    (let [op' (mapv (fn [item]
+                      (if (= (first item) ::db-transact)
+                        (let [m (second item)
+                              tx-data' (vec (:tx-data m))]
+                          (if (seq tx-data')
+                            [::db-transact (assoc m :tx-data tx-data')]
+                            ::db-transact-no-tx-data))
+                        item))
+                    op)]
+      (when-not (some #{::db-transact-no-tx-data} op')
+        op'))))
 
 (defn- empty-undo-stack?
-  [repo page-block-uuid]
-  (empty? (get-in @(:undo/repo->page-block-uuid->undo-ops @worker-state/*state) [repo page-block-uuid])))
+  [repo]
+  (empty? (get @*undo-ops repo)))
 
 (defn- empty-redo-stack?
-  [repo page-block-uuid]
-  (empty? (get-in @(:undo/repo->page-block-uuid->redo-ops @worker-state/*state) [repo page-block-uuid])))
-
-(defn- push-redo-ops
-  [repo page-block-uuid ops]
-  (assert (and (undo-ops-validator ops)
-               (uuid? page-block-uuid))
-          {:ops ops :page-block-uuid page-block-uuid})
-  (swap! (:undo/repo->page-block-uuid->redo-ops @worker-state/*state)
-         update-in [repo page-block-uuid]
-         apply-conj-vec ops))
-
-(defn- pop-redo-ops
-  [repo page-block-uuid]
-  (assert (uuid? page-block-uuid) page-block-uuid)
-  (let [repo->page-block-uuid->redo-ops (:undo/repo->page-block-uuid->redo-ops @worker-state/*state)
-        undo-stack (get-in @repo->page-block-uuid->redo-ops [repo page-block-uuid])
-        [ops undo-stack*] (pop-ops-helper undo-stack)]
-    (swap! repo->page-block-uuid->redo-ops assoc-in [repo page-block-uuid] undo-stack*)
-    ops))
-
-(defn- normal-block?
-  [entity]
-  (and (:block/uuid entity)
-       (:block/parent entity)
-       (:block/left entity)))
-
-(defmulti ^:private reverse-apply-op (fn [op _conn _repo] (first op)))
-(defmethod reverse-apply-op :default
-  [_ _ _]
-  nil)
-
-(defmethod reverse-apply-op ::remove-block
-  [op conn repo]
-  (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))]
-          (outliner-tx/transact!
-           {:gen-undo-ops? false
-            :outliner-op :insert-blocks
-            :transact-opts {:repo repo
-                            :conn conn}}
-           (outliner-core/insert-blocks! repo conn
-                                         [(cond-> {:block/uuid block-uuid
-                                                   :block/title (:block/title 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 (some->> (:block/tags block-entity-map)
-                                                                        (map (partial vector :block/uuid))
-                                                                        (d/pull-many @conn [:db/id])
-                                                                        (keep :db/id))))]
-                                         left-entity {:sibling? sibling? :keep-uuid? true}))
-          (when (d/entity @conn [:block/uuid block-uuid])
-            [:push-undo-redo {}]))))))
+  [repo]
+  (empty? (get @*redo-ops repo)))
+
+(defn- get-moved-blocks
+  [e->datoms]
+  (->>
+   (keep (fn [[e datoms]]
+           (when (some
+                  (fn [k]
+                    (and (some (fn [d] (and (= k (:a d)) (:added d))) datoms)
+                         (some (fn [d] (and (= k (:a d)) (not (:added d)))) datoms)))
+                  [:block/parent :block/order])
+             e)) e->datoms)
+   (set)))
 
 (defn- other-children-exist?
-  "return true if there are other children existing(not included in `block-entities`)"
-  [block-entities]
-  (let [block-uuid-set (set (keep :block/uuid block-entities))]
-    (boolean
-     (some
-      (fn [block-entity]
-        (seq
-         (set/difference
-          (set (keep :block/uuid (:block/_parent block-entity)))
-          block-uuid-set)))
-      block-entities))))
-
-(defmethod reverse-apply-op ::insert-blocks
-  [op conn repo]
-  (let [[_ {:keys [block-uuids]}] op]
-    (when-let [block-entities (->> block-uuids
-                                   (keep #(d/entity @conn [:block/uuid %]))
-                                   not-empty)]
-      (when-not (other-children-exist? block-entities)
-        (outliner-tx/transact!
-         {:gen-undo-ops? 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-entities
-                                       {})))
-
-      (when (every? nil? (map #(d/entity @conn [:block/uuid %]) block-uuids))
-        [:push-undo-redo {}]))))
-
-(defmethod reverse-apply-op ::move-block
-  [op conn repo]
-  (let [[_ {:keys [block-uuid block-origin-left block-origin-parent]}] op]
-    (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-ops? 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 {}])))))
-
-(defmethod reverse-apply-op ::update-block
-  [op conn repo]
-  (let [[_ {:keys [block-uuid block-origin-content
-                   block-origin-tags block-origin-collapsed
-                   block-origin-link]
-            :as origin-value-map}] op]
-    (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
-      (when (normal-block? block-entity)
-        (let [db-id (:db/id block-entity)
-              retract-attrs-tx-data (cond-> []
-                                      (some? block-origin-tags)
-                                      (conj [:db/retract db-id :block/tags])
-
-                                      (and (contains? origin-value-map :block-origin-link)
-                                           (nil? block-origin-link))
-                                      (conj [:db/retract db-id :block/link]))
-              _ (when (seq retract-attrs-tx-data)
-                  (ldb/transact! conn retract-attrs-tx-data {:gen-undo-ops? false}))
-              new-block (cond-> block-entity
-                          (some? block-origin-content)
-                          (assoc :block/title block-origin-content)
-                          (some? block-origin-tags)
-                          (assoc :block/tags (some->> block-origin-tags
-                                                      (map (partial vector :block/uuid))
-                                                      (d/pull-many @conn [:db/id])
-                                                      (keep :db/id)))
-                          (some? block-origin-collapsed)
-                          (assoc :block/collapsed? (boolean block-origin-collapsed))
-                          (some? block-origin-link)
-                          (assoc :block/link [:block/uuid block-origin-link]))
-              _ (outliner-tx/transact!
-                 {:gen-undo-ops? 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 {}])))))
-
-(defmethod reverse-apply-op ::record-editor-info
-  [_op _conn _repo]
-  [:push-undo-redo {}])
-
-(defn- sort&merge-ops
-  [ops]
-  (let [groups            (group-by first ops)
-        remove-ops        (groups ::remove-block)
-        insert-ops        (groups ::insert-blocks)
-        other-ops         (apply concat (vals (dissoc groups ::remove-block ::insert-blocks)))
-        sorted-remove-ops (reverse
-                           (common-util/sort-coll-by-dependency (comp :block-uuid second)
-                                                                (comp :block/left :block-entity-map second)
-                                                                remove-ops))
-        insert-op         (some->> (seq insert-ops)
-                                   (mapcat (fn [op] (:block-uuids (second op))))
-                                   (hash-map :block-uuids)
-                                   (vector ::insert-blocks))
-        conj-vec          (partial apply conj)]
-    (cond-> []
-      insert-op               (conj insert-op)
-      (seq other-ops)         (conj-vec other-ops)
-      (seq sorted-remove-ops) (conj-vec sorted-remove-ops))))
+  "return true if there are other children existing(not included in `ids`)"
+  [entity ids]
+  (seq
+   (set/difference
+    (set (map :db/id (:block/_parent entity)))
+    ids)))
+
+(defn- reverse-datoms
+  [conn datoms schema added-ids retracted-ids undo? redo?]
+  (keep
+   (fn [[e a v _tx add?]]
+     (let [ref? (= :db.type/ref (get-in schema [a :db/valueType]))
+           op (if (or (and redo? add?) (and undo? (not add?)))
+                :db/add
+                :db/retract)]
+       (when (or (not ref?)
+                 (d/entity @conn v)
+                 (and (retracted-ids v) undo?)
+                 (and (added-ids v) redo?)) ; entity exists
+         [op e a v])))
+   datoms))
+
+(defn- moved-block-or-target-deleted?
+  [conn e->datoms e moved-blocks redo?]
+  (let [datoms (get e->datoms e)]
+    (and (moved-blocks e)
+         (let [b (d/entity @conn e)
+               cur-parent (:db/id (:block/parent b))
+               move-datoms (filter (fn [d] (contains? #{:block/parent} (:a d))) datoms)]
+           (when cur-parent
+             (let [before-parent (some (fn [d] (when (and (= :block/parent (:a d)) (not (:added d))) (:v d))) move-datoms)
+                   after-parent (some (fn [d] (when (and (= :block/parent (:a d)) (:added d)) (:v d))) move-datoms)]
+               (and before-parent after-parent ; parent changed
+                    (if redo?
+                      (or (not= cur-parent before-parent)
+                          (nil? (d/entity @conn after-parent)))
+                      (or (not= cur-parent after-parent)
+                          (nil? (d/entity @conn before-parent)))))))))))
+
+(defn get-reversed-datoms
+  [conn undo? {:keys [tx-data added-ids retracted-ids] :as op} _tx-meta]
+  (try
+    (let [redo? (not undo?)
+          e->datoms (->> (if redo? tx-data (reverse tx-data))
+                         (group-by :e))
+          schema (:schema @conn)
+          added-and-retracted-ids (set/union added-ids retracted-ids)
+          moved-blocks (get-moved-blocks e->datoms)]
+      (->>
+       (mapcat
+        (fn [[e datoms]]
+          (let [entity (d/entity @conn e)]
+            (cond
+              ;; entity has been deleted
+              (and (nil? entity)
+                   (not (contains? added-and-retracted-ids e)))
+              (throw (ex-info "Entity has been deleted"
+                              (merge op {:error :entity-deleted
+                                         :undo? undo?})))
+
+              ;; new children blocks have been added
+              (or (and (contains? retracted-ids e) redo?
+                       (other-children-exist? entity retracted-ids)) ; redo delete-blocks
+                  (and (contains? added-ids e) undo?                 ; undo insert-blocks
+                       (other-children-exist? entity added-ids)))
+              (throw (ex-info "Children still exists"
+                              (merge op {:error :block-children-exists
+                                         :undo? undo?})))
+
+              ;; block has been moved or target got deleted by another client
+              (moved-block-or-target-deleted? conn e->datoms e moved-blocks redo?)
+              (throw (ex-info "This block has been moved or its target has been deleted"
+                              (merge op {:error :block-moved-or-target-deleted
+                                         :undo? undo?})))
+
+              ;; The entity should be deleted instead of retracting its attributes
+              (and entity
+                   (or (and (contains? retracted-ids e) redo?) ; redo delete-blocks
+                       (and (contains? added-ids e) undo?)))   ; undo insert-blocks
+              [[:db/retractEntity e]]
+
+              ;; reverse datoms
+              :else
+              (reverse-datoms conn datoms schema added-ids retracted-ids undo? redo?))))
+        e->datoms)
+       (remove nil?)))
+    (catch :default e
+      (prn :debug :undo-redo :error (:error (ex-data e)))
+      (when-not (contains? #{:entity-deleted
+                             :block-moved-or-target-deleted
+                             :block-children-exists}
+                           (:error (ex-data e)))
+        (throw e)))))
+
+(defn- undo-redo-aux
+  [repo conn undo?]
+  (if-let [op (not-empty ((if undo? pop-undo-op pop-redo-op) repo))]
+    (cond
+      (= ::ui-state (ffirst op))
+      (do
+        ((if undo? push-redo-op push-undo-op) repo op)
+        (let [ui-state-str (second (first op))]
+          {:undo? undo?
+           :ui-state-str ui-state-str}))
+
+      :else
+      (let [{:keys [tx-data tx-meta] :as data} (some #(when (= ::db-transact (first %))
+                                                        (second %)) op)]
+        (when (seq tx-data)
+          (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
+                tx-meta' (-> tx-meta
+                             (dissoc :pipeline-replace?
+                                     :batch-tx/batch-tx-mode?)
+                             (assoc
+                              :gen-undo-ops? false
+                              :undo? undo?))]
+            (when (seq reversed-tx-data)
+              (ldb/transact! conn reversed-tx-data tx-meta')
+              ((if undo? push-redo-op push-undo-op) repo op)
+              (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op)
+                                        (map second))
+                    block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid
+                                                                              (if undo?
+                                                                                (first editor-cursors)
+                                                                                (last editor-cursors)))]))]
+                {:undo? undo?
+                 :editor-cursors editor-cursors
+                 :block-content block-content}))))))
+
+    (when ((if undo? empty-undo-stack? empty-redo-stack?) repo)
+      (prn (str "No further " (if undo? "undo" "redo") " information"))
+      (if undo? ::empty-undo-stack ::empty-redo-stack))))
 
 (defn undo
-  [repo page-block-uuid conn]
-  (if-let [ops (not-empty (pop-undo-ops repo page-block-uuid))]
-    (let [redo-ops-to-push (transient [])]
-      (batch-tx/with-batch-tx-mode conn {:gen-undo-ops? false
-                                         :undo? true}
-        (doseq [op ops]
-          (let [rev-ops (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)}))
-              (apply conj! redo-ops-to-push rev-ops)))))
-      (when-let [rev-ops (not-empty (sort&merge-ops (persistent! redo-ops-to-push)))]
-        (push-redo-ops repo page-block-uuid (vec (cons boundary rev-ops))))
-      (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) ops)
-                                (map second)
-                                (reverse))
-            block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid (first editor-cursors))]))]
-        {:undo? true
-         :editor-cursors editor-cursors
-         :block-content block-content}))
-
-    (when (empty-undo-stack? repo page-block-uuid)
-      (prn "No further undo information")
-      ::empty-undo-stack)))
+  [repo conn]
+  (undo-redo-aux repo conn true))
 
 (defn redo
-  [repo page-block-uuid conn]
-  (if-let [ops (not-empty (pop-redo-ops repo page-block-uuid))]
-    (let [undo-ops-to-push (transient [])]
-      (batch-tx/with-batch-tx-mode conn {:gen-undo-ops? false
-                                         :redo? true}
-        (doseq [op ops]
-          (let [rev-ops (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)}))
-              (apply conj! undo-ops-to-push rev-ops)))))
-      (when-let [rev-ops (not-empty (sort&merge-ops (persistent! undo-ops-to-push)))]
-        (push-undo-ops repo page-block-uuid (vec (cons boundary rev-ops))))
-      (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) ops)
-                                (map second))
-            block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid (last editor-cursors))]))]
-        {:redo? true
-         :editor-cursors editor-cursors
-         :block-content block-content}))
-
-    (when (empty-redo-stack? repo page-block-uuid)
-      (prn "No further redo information")
-      ::empty-redo-stack)))
-
-;;; listen db changes and push undo-ops
-
-(defn- entity-datoms=>ops
-  [db-before db-after id->attr->datom entity-datoms]
-  (when-let [e (ffirst entity-datoms)]
-    (let [attr->datom (id->attr->datom e)]
-      (when (seq attr->datom)
-        (let [updated-key-set (set (keys attr->datom))
-              {[_ _ block-uuid _ add1?]    :block/uuid
-               [_ _ _ _ add3?]             :block/left
-               [_ _ _ _ add4?]             :block/parent} attr->datom
-              entity-before (d/entity db-before e)
-              entity-after (d/entity db-after e)
-              ops
-              (cond
-                (and (not add1?) block-uuid
-                     (normal-block? entity-before))
-                [[::remove-block
-                  {:block-uuid (:block/uuid entity-before)
-                   :block-entity-map (->block-entity-map db-before e)}]]
-
-                (and add1? block-uuid
-                     (normal-block? entity-after))
-                [[::insert-blocks {:block-uuids [(:block/uuid entity-after)]}]]
-
-                (and (or add3? add4?)
-                     (normal-block? entity-after))
-                (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))]
-                  (when origin-left-and-parent-available-in-db-after?
-                    [[::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))}]])))
-              other-ops
-              (let [updated-attrs (seq (set/intersection
-                                        updated-key-set
-                                        #{:block/title :block/tags :block/collapsed? :block/link}))]
-                (when-let [update-block-op-value
-                           (when (normal-block? entity-after)
-                             (some->> updated-attrs
-                                      (keep
-                                       (fn [attr-name]
-                                         (case attr-name
-                                           :block/title
-                                           (when-let [origin-content (:block/title entity-before)]
-                                             [:block-origin-content origin-content])
-
-                                           :block/tags
-                                           [:block-origin-tags (mapv :block/uuid (:block/tags entity-before))]
-
-                                           :block/collapsed?
-                                           [:block-origin-collapsed (boolean (:block/collapsed? entity-before))]
-
-                                           :block/link
-                                           [:block-origin-link (:block/uuid (:block/link entity-before))]
-
-                                           nil)))
-                                      seq
-                                      (into {:block-uuid (:block/uuid entity-after)})))]
-                  [[::update-block update-block-op-value]]))]
-          (concat ops other-ops))))))
-
-(defn- find-page-block-uuid
-  [db-before db-after same-entity-datoms-coll]
-  (some
-   (fn [entity-datoms]
-     (when-let [e (ffirst entity-datoms)]
-       (or (some-> (d/entity db-before e) :block/page :block/uuid)
-           (some-> (d/entity db-after e) :block/page :block/uuid))))
-   same-entity-datoms-coll))
-
-(defn- generate-undo-ops
-  [repo db-before db-after same-entity-datoms-coll id->attr->datom gen-boundary-op? tx-meta]
-  (when-let [page-block-uuid (find-page-block-uuid db-before db-after same-entity-datoms-coll)]
-    (let [ops (mapcat (partial entity-datoms=>ops db-before db-after id->attr->datom) same-entity-datoms-coll)
-          ops (sort&merge-ops ops)
-          editor-info (:editor-info tx-meta)
-          ops' (if editor-info
-                 (cons [::record-editor-info editor-info] ops)
-                 ops)]
-      (when (seq ops)
-        (push-undo-ops repo page-block-uuid (if gen-boundary-op? (vec (cons boundary ops')) ops'))))))
+  [repo conn]
+  (undo-redo-aux repo conn false))
+
+(defn record-editor-info!
+  [repo editor-info]
+  (swap! *undo-ops
+         update repo
+         (fn [stack]
+           (if (seq stack)
+             (update stack (dec (count stack))
+                     (fn [op]
+                       (conj (vec op) [::record-editor-info editor-info])))
+             stack))))
+
+(defn record-ui-state!
+  [repo ui-state-str]
+  (when ui-state-str
+    (push-undo-op repo [[::ui-state ui-state-str]])))
 
 (defmethod db-listener/listen-db-changes :gen-undo-ops
-  [_
-   {:keys [repo id->attr->datom same-entity-datoms-coll]}
-   {:keys [_tx-data tx-meta db-before db-after]}]
-  (when (:gen-undo-ops? tx-meta true)
-    (generate-undo-ops repo db-before db-after same-entity-datoms-coll id->attr->datom
-                       (:gen-undo-boundary-op? tx-meta true)
-                       tx-meta)))
-
-(comment
-  (defn record-editor-info!
-    [repo page-block-uuid editor-info]
-    (swap! (:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
-           update-in [repo page-block-uuid]
-           (fn [stack]
-             (when (seq stack)
-               (conj (vec stack) [::record-editor-info editor-info]))))))
-
-;;; listen db changes and push undo-ops (ends)
-
-(defn clear-undo-redo-stack
-  []
-  (reset! (:undo/repo->page-block-uuid->redo-ops @worker-state/*state) {})
-  (reset! (:undo/repo->page-block-uuid->undo-ops @worker-state/*state) {}))
-
-(comment
-
-  (clear-undo-redo-stack)
-  (add-watch (:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
-             :xxx
-             (fn [_ _ o n]
-               (cljs.pprint/pprint {:k :undo
-                                    :o o
-                                    :n n})))
-
-  (add-watch (:undo/repo->page-block-uuid->redo-ops @worker-state/*state)
-             :xxx
-             (fn [_ _ o n]
-               (cljs.pprint/pprint {:k :redo
-                                    :o o
-                                    :n n})))
-
-  (remove-watch (:undo/repo->page-block-uuid->undo-ops @worker-state/*state) :xxx)
-  (remove-watch (:undo/repo->page-block-uuid->redo-ops @worker-state/*state) :xxx))
+  [_ {:keys [repo]} {:keys [tx-data tx-meta db-after db-before]}]
+  (let [{:keys [outliner-op]} tx-meta]
+    (when (and outliner-op (not (false? (:gen-undo-ops? tx-meta)))
+               (not (:create-today-journal? tx-meta)))
+      (let [editor-info (:editor-info tx-meta)
+            all-ids (distinct (map :e tx-data))
+            retracted-ids (set
+                           (filter
+                            (fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
+                            all-ids))
+            added-ids (set
+                       (filter
+                        (fn [id] (and (nil? (d/entity db-before id)) (d/entity db-after id)))
+                        all-ids))
+            tx-data' (->> (remove (fn [d] (contains? #{:block/path-refs} (:a d))) tx-data)
+                          vec)
+            op (->> [(when editor-info [::record-editor-info editor-info])
+                     [::db-transact
+                      {:tx-data tx-data'
+                       :tx-meta tx-meta
+                       :added-ids added-ids
+                       :retracted-ids retracted-ids}]]
+                    (remove nil?)
+                    vec)]
+        (push-undo-op repo op)))))

+ 0 - 340
src/main/frontend/worker/undo_redo2.cljs

@@ -1,340 +0,0 @@
-(ns frontend.worker.undo-redo2
-  "Undo redo new implementation"
-  (:require [clojure.set :as set]
-            [datascript.core :as d]
-            [frontend.worker.db-listener :as db-listener]
-            [frontend.worker.state :as worker-state]
-            [logseq.common.defkeywords :refer [defkeywords]]
-            [logseq.db :as ldb]
-            [malli.core :as m]
-            [malli.util :as mu]))
-
-(defkeywords
-  ::record-editor-info {:doc "record current editor and cursor"}
-  ::db-transact {:doc "db tx"}
-  ::ui-state {:doc "ui state such as route && sidebar blocks"})
-
-;; TODO: add other UI states such as `::ui-updates`.
-(comment
-  ;; TODO: convert it to a qualified-keyword
-  (sr/defkeyword :gen-undo-ops?
-    "tx-meta option, generate undo ops from tx-data when true (default true)"))
-
-(def ^:private undo-op-item-schema
-  (mu/closed-schema
-   [:multi {:dispatch first}
-    [::db-transact
-     [:cat :keyword
-      [:map
-       [:tx-data [:sequential [:fn
-                               {:error/message "should be a Datom"}
-                               d/datom?]]]
-       [:tx-meta [:map {:closed false}
-                  [:outliner-op :keyword]]]
-       [:added-ids [:set :int]]
-       [:retracted-ids [:set :int]]]]]
-
-    [::record-editor-info
-     [:cat :keyword
-      [:map
-       [:block-uuid :uuid]
-       [:container-id [:or :int [:enum :unknown-container]]]
-       [:start-pos [:maybe :int]]
-       [:end-pos [:maybe :int]]]]]
-
-    [::ui-state
-     [:cat :keyword :string]]]))
-
-(def ^:private undo-op-validator (m/validator [:sequential undo-op-item-schema]))
-
-(defonce max-stack-length 100)
-(defonce *undo-ops (:undo/repo->ops @worker-state/*state))
-(defonce *redo-ops (:redo/repo->ops @worker-state/*state))
-
-(defn- conj-op
-  [col op]
-  (let [result (conj (if (empty? col) [] col) op)]
-    (if (>= (count result) max-stack-length)
-      (subvec result 0 (/ max-stack-length 2))
-      result)))
-
-(defn- pop-stack
-  [stack]
-  (when (seq stack)
-    [(last stack) (pop stack)]))
-
-(defn- push-undo-op
-  [repo op]
-  (assert (undo-op-validator op) {:op op})
-  (swap! *undo-ops update repo conj-op op))
-
-(defn- push-redo-op
-  [repo op]
-  (assert (undo-op-validator op) {:op op})
-  (swap! *redo-ops update repo conj-op op))
-
-(comment
-  ;; This version checks updated datoms by other clients, allows undo and redo back
-  ;; to the current state.
-  ;; The downside is that it'll undo the changes made by others.
-  (defn- pop-undo-op
-    [repo conn]
-    (let [undo-stack (get @*undo-ops repo)
-          [op undo-stack*] (pop-stack undo-stack)]
-      (swap! *undo-ops assoc repo undo-stack*)
-      (mapv (fn [item]
-              (if (= (first item) ::db-transact)
-                (let [m (second item)
-                      tx-data' (mapv
-                                (fn [{:keys [e a v tx add] :as datom}]
-                                  (let [one-value? (= :db.cardinality/one (:db/cardinality (d/entity @conn a)))
-                                        new-value (when (and one-value? add) (get (d/entity @conn e) a))
-                                        value-not-matched? (and (some? new-value) (not= v new-value))]
-                                    (if value-not-matched?
-                                    ;; another client might updated `new-value`, the datom below will be used
-                                    ;; to restore the the current state when redo this undo.
-                                      (d/datom e a new-value tx add)
-                                      datom)))
-                                (:tx-data m))]
-                  [::db-transact (assoc m :tx-data tx-data')])
-                item))
-            op))))
-
-(defn- pop-undo-op
-  [repo]
-  (let [undo-stack (get @*undo-ops repo)
-        [op undo-stack*] (pop-stack undo-stack)]
-    (swap! *undo-ops assoc repo undo-stack*)
-    (let [op' (mapv (fn [item]
-                      (if (= (first item) ::db-transact)
-                        (let [m (second item)
-                              tx-data' (vec (:tx-data m))]
-                          (if (seq tx-data')
-                            [::db-transact (assoc m :tx-data tx-data')]
-                            ::db-transact-no-tx-data))
-                        item))
-                    op)]
-      (when-not (some #{::db-transact-no-tx-data} op')
-        op'))))
-
-(defn- pop-redo-op
-  [repo]
-  (let [redo-stack (get @*redo-ops repo)
-        [op redo-stack*] (pop-stack redo-stack)]
-    (swap! *redo-ops assoc repo redo-stack*)
-    (let [op' (mapv (fn [item]
-                      (if (= (first item) ::db-transact)
-                        (let [m (second item)
-                              tx-data' (vec (:tx-data m))]
-                          (if (seq tx-data')
-                            [::db-transact (assoc m :tx-data tx-data')]
-                            ::db-transact-no-tx-data))
-                        item))
-                    op)]
-      (when-not (some #{::db-transact-no-tx-data} op')
-        op'))))
-
-(defn- empty-undo-stack?
-  [repo]
-  (empty? (get @*undo-ops repo)))
-
-(defn- empty-redo-stack?
-  [repo]
-  (empty? (get @*redo-ops repo)))
-
-(defn- get-moved-blocks
-  [e->datoms]
-  (->>
-   (keep (fn [[e datoms]]
-           (when (some
-                  (fn [k]
-                    (and (some (fn [d] (and (= k (:a d)) (:added d))) datoms)
-                         (some (fn [d] (and (= k (:a d)) (not (:added d)))) datoms)))
-                  [:block/parent :block/order])
-             e)) e->datoms)
-   (set)))
-
-(defn- other-children-exist?
-  "return true if there are other children existing(not included in `ids`)"
-  [entity ids]
-  (seq
-   (set/difference
-    (set (map :db/id (:block/_parent entity)))
-    ids)))
-
-(defn- reverse-datoms
-  [conn datoms schema added-ids retracted-ids undo? redo?]
-  (keep
-   (fn [[e a v _tx add?]]
-     (let [ref? (= :db.type/ref (get-in schema [a :db/valueType]))
-           op (if (or (and redo? add?) (and undo? (not add?)))
-                :db/add
-                :db/retract)]
-       (when (or (not ref?)
-                 (d/entity @conn v)
-                 (and (retracted-ids v) undo?)
-                 (and (added-ids v) redo?)) ; entity exists
-         [op e a v])))
-   datoms))
-
-(defn- moved-block-or-target-deleted?
-  [conn e->datoms e moved-blocks redo?]
-  (let [datoms (get e->datoms e)]
-    (and (moved-blocks e)
-         (let [b (d/entity @conn e)
-               cur-parent (:db/id (:block/parent b))
-               move-datoms (filter (fn [d] (contains? #{:block/parent} (:a d))) datoms)]
-           (when cur-parent
-             (let [before-parent (some (fn [d] (when (and (= :block/parent (:a d)) (not (:added d))) (:v d))) move-datoms)
-                   after-parent (some (fn [d] (when (and (= :block/parent (:a d)) (:added d)) (:v d))) move-datoms)]
-               (and before-parent after-parent ; parent changed
-                    (if redo?
-                      (or (not= cur-parent before-parent)
-                          (nil? (d/entity @conn after-parent)))
-                      (or (not= cur-parent after-parent)
-                          (nil? (d/entity @conn before-parent)))))))))))
-
-(defn get-reversed-datoms
-  [conn undo? {:keys [tx-data added-ids retracted-ids] :as op} _tx-meta]
-  (try
-    (let [redo? (not undo?)
-          e->datoms (->> (if redo? tx-data (reverse tx-data))
-                         (group-by :e))
-          schema (:schema @conn)
-          added-and-retracted-ids (set/union added-ids retracted-ids)
-          moved-blocks (get-moved-blocks e->datoms)]
-      (->>
-       (mapcat
-        (fn [[e datoms]]
-          (let [entity (d/entity @conn e)]
-            (cond
-              ;; entity has been deleted
-              (and (nil? entity)
-                   (not (contains? added-and-retracted-ids e)))
-              (throw (ex-info "Entity has been deleted"
-                              (merge op {:error :entity-deleted
-                                         :undo? undo?})))
-
-              ;; new children blocks have been added
-              (or (and (contains? retracted-ids e) redo?
-                       (other-children-exist? entity retracted-ids)) ; redo delete-blocks
-                  (and (contains? added-ids e) undo?                 ; undo insert-blocks
-                       (other-children-exist? entity added-ids)))
-              (throw (ex-info "Children still exists"
-                              (merge op {:error :block-children-exists
-                                         :undo? undo?})))
-
-              ;; block has been moved or target got deleted by another client
-              (moved-block-or-target-deleted? conn e->datoms e moved-blocks redo?)
-              (throw (ex-info "This block has been moved or its target has been deleted"
-                              (merge op {:error :block-moved-or-target-deleted
-                                         :undo? undo?})))
-
-              ;; The entity should be deleted instead of retracting its attributes
-              (and entity
-                   (or (and (contains? retracted-ids e) redo?) ; redo delete-blocks
-                       (and (contains? added-ids e) undo?)))   ; undo insert-blocks
-              [[:db/retractEntity e]]
-
-              ;; reverse datoms
-              :else
-              (reverse-datoms conn datoms schema added-ids retracted-ids undo? redo?))))
-        e->datoms)
-       (remove nil?)))
-    (catch :default e
-      (prn :debug :undo-redo :error (:error (ex-data e)))
-      (when-not (contains? #{:entity-deleted
-                             :block-moved-or-target-deleted
-                             :block-children-exists}
-                           (:error (ex-data e)))
-        (throw e)))))
-
-(defn- undo-redo-aux
-  [repo conn undo?]
-  (if-let [op (not-empty ((if undo? pop-undo-op pop-redo-op) repo))]
-    (cond
-      (= ::ui-state (ffirst op))
-      (do
-        ((if undo? push-redo-op push-undo-op) repo op)
-        (let [ui-state-str (second (first op))]
-          {:undo? undo?
-           :ui-state-str ui-state-str}))
-
-      :else
-      (let [{:keys [tx-data tx-meta] :as data} (some #(when (= ::db-transact (first %))
-                                                        (second %)) op)]
-        (when (seq tx-data)
-          (let [reversed-tx-data (get-reversed-datoms conn undo? data tx-meta)
-                tx-meta' (-> tx-meta
-                             (dissoc :pipeline-replace?
-                                     :batch-tx/batch-tx-mode?)
-                             (assoc
-                              :gen-undo-ops? false
-                              :undo? undo?))]
-            (when (seq reversed-tx-data)
-              (ldb/transact! conn reversed-tx-data tx-meta')
-              ((if undo? push-redo-op push-undo-op) repo op)
-              (let [editor-cursors (->> (filter #(= ::record-editor-info (first %)) op)
-                                        (map second))
-                    block-content (:block/title (d/entity @conn [:block/uuid (:block-uuid
-                                                                              (if undo?
-                                                                                (first editor-cursors)
-                                                                                (last editor-cursors)))]))]
-                {:undo? undo?
-                 :editor-cursors editor-cursors
-                 :block-content block-content}))))))
-
-    (when ((if undo? empty-undo-stack? empty-redo-stack?) repo)
-      (prn (str "No further " (if undo? "undo" "redo") " information"))
-      (if undo? ::empty-undo-stack ::empty-redo-stack))))
-
-(defn undo
-  [repo conn]
-  (undo-redo-aux repo conn true))
-
-(defn redo
-  [repo conn]
-  (undo-redo-aux repo conn false))
-
-(defn record-editor-info!
-  [repo editor-info]
-  (swap! *undo-ops
-         update repo
-         (fn [stack]
-           (if (seq stack)
-             (update stack (dec (count stack))
-                     (fn [op]
-                       (conj (vec op) [::record-editor-info editor-info])))
-             stack))))
-
-(defn record-ui-state!
-  [repo ui-state-str]
-  (when ui-state-str
-    (push-undo-op repo [[::ui-state ui-state-str]])))
-
-(defmethod db-listener/listen-db-changes :gen-undo-ops
-  [_ {:keys [repo]} {:keys [tx-data tx-meta db-after db-before]}]
-  (let [{:keys [outliner-op]} tx-meta]
-    (when (and outliner-op (not (false? (:gen-undo-ops? tx-meta)))
-               (not (:create-today-journal? tx-meta)))
-      (let [editor-info (:editor-info tx-meta)
-            all-ids (distinct (map :e tx-data))
-            retracted-ids (set
-                           (filter
-                            (fn [id] (and (nil? (d/entity db-after id)) (d/entity db-before id)))
-                            all-ids))
-            added-ids (set
-                       (filter
-                        (fn [id] (and (nil? (d/entity db-before id)) (d/entity db-after id)))
-                        all-ids))
-            tx-data' (->> (remove (fn [d] (contains? #{:block/path-refs} (:a d))) tx-data)
-                          vec)
-            op (->> [(when editor-info [::record-editor-info editor-info])
-                     [::db-transact
-                      {:tx-data tx-data'
-                       :tx-meta tx-meta
-                       :added-ids added-ids
-                       :retracted-ids retracted-ids}]]
-                    (remove nil?)
-                    vec)]
-        (push-undo-op repo op)))))

+ 0 - 3
src/test/frontend/worker/fixtures.cljs

@@ -4,16 +4,13 @@
             [frontend.db.conn :as conn]
             [frontend.test.helper :as test-helper]
             [frontend.worker.db-listener :as worker-db-listener]
-            [frontend.worker.undo-redo :as worker-undo-redo]
             [logseq.db.sqlite.util :as sqlite-util]))
 
-
 (defn listen-test-db-fixture
   [handler-keys]
   (fn [f]
     (let [test-db-conn (conn/get-db test-helper/test-db-name-db-version false)]
       (assert (some? test-db-conn))
-      (worker-undo-redo/clear-undo-redo-stack)
       (worker-db-listener/listen-db-changes! test-helper/test-db-name-db-version test-db-conn
                                              {:handler-keys handler-keys})
 

+ 0 - 73
src/test/frontend/worker/undo_redo2_test.cljs

@@ -1,73 +0,0 @@
-(ns frontend.worker.undo-redo2-test
-  (:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
-            [datascript.core :as d]
-            [frontend.db :as db]
-            [frontend.test.helper :as test-helper]
-            [frontend.worker.undo-redo2 :as undo-redo2]
-            [frontend.modules.outliner.core-test :as outliner-test]
-            [frontend.test.fixtures :as fixtures]
-            [frontend.worker.db-listener :as worker-db-listener]
-            [frontend.state :as state]))
-
-;; TODO: random property ops test
-
-(def test-db test-helper/test-db)
-
-(defn listen-db-fixture
-  [f]
-  (let [test-db-conn (db/get-db test-db false)]
-    (assert (some? test-db-conn))
-    (worker-db-listener/listen-db-changes! test-db test-db-conn
-                                           {:handler-keys [:gen-undo-ops]})
-
-    (f)
-    (d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
-
-(defn disable-browser-fns
-  [f]
-  ;; get-selection-blocks has a js/document reference
-  (with-redefs [state/get-selection-blocks (constantly [])]
-    (f)))
-
-(use-fixtures :each
-  disable-browser-fns
-  fixtures/react-components
-  fixtures/reset-db
-  listen-db-fixture)
-
-(defn- undo-all!
-  [conn]
-  (loop [i 0]
-    (let [r (undo-redo2/undo test-db conn)]
-      (if (not= :frontend.worker.undo-redo2/empty-undo-stack r)
-        (recur (inc i))
-        (prn :undo-count i)))))
-
-(defn- redo-all!
-  [conn]
-  (loop [i 0]
-          (let [r (undo-redo2/redo test-db conn)]
-            (if (not= :frontend.worker.undo-redo2/empty-redo-stack r)
-              (recur (inc i))
-              (prn :redo-count i)))))
-
-(defn- get-datoms
-  [db]
-  (set (map (fn [d] [(:e d) (:a d) (:v d)]) (d/datoms db :eavt))))
-
-(deftest ^:long undo-redo-test
-  (testing "Random mixed operations"
-    (set! undo-redo2/max-stack-length 500)
-    (let [*random-blocks (atom (outliner-test/get-blocks-ids))]
-      (outliner-test/transact-random-tree!)
-      (let [conn (db/get-db false)
-            _ (outliner-test/run-random-mixed-ops! *random-blocks)
-            db-after @conn]
-
-        (undo-all! conn)
-
-        (is (= (get-datoms @conn) #{}))
-
-        (redo-all! conn)
-
-        (is (= (get-datoms @conn) (get-datoms db-after)))))))

+ 54 - 282
src/test/frontend/worker/undo_redo_test.cljs

@@ -1,301 +1,73 @@
 (ns frontend.worker.undo-redo-test
-  (:require ["fs" :as fs-node]
-            [cljs.pprint :as pp]
-            [clojure.test :as t :refer [deftest is testing use-fixtures]]
-            [clojure.test.check.generators :as gen]
-            [clojure.walk :as walk]
+  (:require [clojure.test :as t :refer [deftest is testing use-fixtures]]
             [datascript.core :as d]
             [frontend.db :as db]
-            [frontend.test.generators :as t.gen]
+            [frontend.modules.outliner.core-test :as outliner-test]
+            [frontend.state :as state]
+            [frontend.test.fixtures :as fixtures]
             [frontend.test.helper :as test-helper]
-            [frontend.worker.fixtures :as worker-fixtures]
-            [frontend.worker.state :as worker-state]
-            [frontend.worker.undo-redo :as undo-redo]
-            [logseq.db :as ldb]
-            [logseq.db.sqlite.util :as sqlite-util]
-            [logseq.outliner.op :as outliner-op]
-            [logseq.outliner.tree :as otree]))
+            [frontend.worker.db-listener :as worker-db-listener]
+            [frontend.worker.undo-redo :as undo-redo]))
 
-(def ^:private page-uuid (random-uuid))
-(def ^:private init-data (test-helper/initial-test-page-and-blocks {:page-uuid page-uuid}))
+;; TODO: random property ops test
 
-(defn- start-and-destroy-db
-  [f]
-  (test-helper/db-based-start-and-destroy-db
-   f
-   {:init-data (fn [conn] (d/transact! conn init-data {:gen-undo-ops? false}))}))
-
-(use-fixtures :each
-  start-and-destroy-db
-  (worker-fixtures/listen-test-db-fixture [:gen-undo-ops])
-  worker-fixtures/listen-test-db-to-write-tx-log-json-file)
-
-(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 {:page-uuid page-uuid})]
-                  [non-exist-frequency gen-non-exist-block-uuid]]))
-
-(defn- gen-parent-left-pair
-  [db self-uuid]
-  (gen/such-that
-   (fn [[parent left]]
-     (and (not= self-uuid left)
-          (not= self-uuid parent)))
-   (gen/frequency [[9 (t.gen/gen-available-parent db {:page-uuid page-uuid})]
-                   [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 block-uuid)]
-    [: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-blocks
-     {:block-uuids [block-uuid]}]))
-
-(defn- gen-remove-block-op
-  [db]
-  (gen/let [block-uuid (gen-block-uuid db {:non-exist-frequency 90})
-            [parent left] (gen-parent-left-pair db block-uuid)
-            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/title content}}]))
-
-(defn- gen-update-block-op
-  [db]
-  (let [gen-content-attr (gen/let [content gen/string-alphanumeric]
-                           [:block-origin-content content])
-        gen-collapsed-attr (gen/let [v gen/boolean]
-                             [:block-origin-collapsed v])
-        gen-tags-attr (gen/let [tags (gen/vector (gen-block-uuid db))]
-                        [:block-origin-tags tags])]
-    (gen/let [block-uuid (gen-block-uuid db)
-              attrs (gen/vector (gen/one-of [gen-content-attr gen-collapsed-attr gen-tags-attr]) 3)]
-      [:frontend.worker.undo-redo/update-block
-       (into {:block-uuid block-uuid} attrs)])))
-
-(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]]))
+(def test-db test-helper/test-db)
 
-(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 :entity (into {} (d/entity current-db [:block/uuid (:block-uuid (second op))]))})
-
-    :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-blocks
-    (let [entities (map #(d/entity current-db [:block/uuid %]) (:block-uuids (second op)))]
-      (assert (every? nil? entities)
-              {:op op :tx-data (:tx-data tx) :x (keys tx)}))
+(defn listen-db-fixture
+  [f]
+  (let [test-db-conn (db/get-db test-db false)]
+    (assert (some? test-db-conn))
+    (worker-db-listener/listen-db-changes! test-db test-db-conn
+                                           {:handler-keys [:gen-undo-ops]})
 
-    :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))
+    (f)
+    (d/unlisten! test-db-conn :frontend.worker.db-listener/listen-db-changes!)))
 
-(defn- undo-all
-  [conn page-uuid']
-  (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 page-uuid' 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))))))
+(defn disable-browser-fns
+  [f]
+  ;; get-selection-blocks has a js/document reference
+  (with-redefs [state/get-selection-blocks (constantly [])]
+    (f)))
 
-(defn- redo-all
-  [conn page-uuid']
-  (binding [undo-redo/*undo-redo-info-for-test* (atom nil)]
-    (loop [i 0]
-      (let [r (undo-redo/redo test-helper/test-db-name-db-version page-uuid' conn)
-            current-db @conn]
-        (check-block-count @undo-redo/*undo-redo-info-for-test* current-db)
-        (if (not= :frontend.worker.undo-redo/empty-redo-stack r)
-          (recur (inc i))
-          (prn :redo-count i))))))
+(use-fixtures :each
+  disable-browser-fns
+  fixtures/react-components
+  fixtures/reset-db
+  listen-db-fixture)
 
-(defn- undo-all-then-redo-all
+(defn- undo-all!
   [conn]
-  (undo-all conn page-uuid)
-  (redo-all conn page-uuid))
-
-(deftest ^:long ^:fix-me undo-redo-gen-test
-  (let [conn (db/get-db false)
-        all-remove-ops (gen/generate (gen/vector (gen-op @conn {:remove-block-op 1000}) 1000))]
-    (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version page-uuid 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 page-uuid 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}) 1000))]
-        (prn :generate-move-ops (count ops))
-        (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version page-uuid ops)
+  (loop [i 0]
+    (let [r (undo-redo/undo test-db conn)]
+      (if (not= :frontend.worker.undo-redo/empty-undo-stack r)
+        (recur (inc i))
+        (prn :undo-count i)))))
 
-        (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 :generate-random-ops (count ops))
-        (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version page-uuid 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)))))))
-
-(defn- print-page-stat
-  [db page-uuid']
-  (let [page (d/entity db [:block/uuid page-uuid'])
-        blocks (ldb/get-page-blocks db (:db/id page))]
-    (pp/pprint
-     {:block-count (count blocks)
-      :undo-op-count (count (get-in @(:undo/repo->page-block-uuid->undo-ops @worker-state/*state)
-                                    [test-helper/test-db-name-db-version page-uuid']))
-      :redo-op-count (count (get-in @(:undo/repo->page-block-uuid->redo-ops @worker-state/*state)
-                                    [test-helper/test-db-name-db-version page-uuid']))})))
+(defn- redo-all!
+  [conn]
+  (loop [i 0]
+    (let [r (undo-redo/redo test-db conn)]
+      (if (not= :frontend.worker.undo-redo/empty-redo-stack r)
+        (recur (inc i))
+        (prn :redo-count i)))))
 
-(defn- print-page-blocks-tree
-  [db page-uuid']
-  (let [page (d/entity db [:block/uuid page-uuid'])
-        blocks (ldb/get-page-blocks db (:db/id page))]
-    (prn ::page-block-tree)
-    (pp/pprint
-     (walk/postwalk
-      (fn [x]
-        (if (map? x)
-          (cond-> (select-keys x [:db/id])
-            (seq (:block/children x))
-            (assoc :block/children (:block/children x)))
-          x))
-      (otree/blocks->vec-tree test-helper/test-db-name-db-version db
-                              blocks page-uuid')))))
+(defn- get-datoms
+  [db]
+  (set (map (fn [d] [(:e d) (:a d) (:v d)]) (d/datoms db :eavt))))
 
-(deftest ^:long ^:fix-me undo-redo-outliner-op-gen-test
-  (try
-    (let [conn (db/get-db false)]
-      (loop [num 100]
-        (when (> num 0)
-          (if-let [op (gen/generate (t.gen/gen-insert-blocks-op @conn {:page-uuid page-uuid}))]
-            (do (outliner-op/apply-ops! test-helper/test-db-name-db-version conn
-                                        [op] "MMM do, yyyy" nil)
-                (recur (dec num)))
-            (recur (dec num)))))
-      (println "================ random inserts ================")
-      (print-page-stat @conn page-uuid)
-      (undo-all conn page-uuid)
-      (print-page-stat @conn page-uuid)
-      (redo-all conn page-uuid)
-      (print-page-stat @conn page-uuid)
+(deftest ^:long undo-redo-test
+  (testing "Random mixed operations"
+    (set! undo-redo/max-stack-length 500)
+    (let [*random-blocks (atom (outliner-test/get-blocks-ids))]
+      (outliner-test/transact-random-tree!)
+      (let [conn (db/get-db false)
+            _ (outliner-test/run-random-mixed-ops! *random-blocks)
+            db-after @conn]
 
-      (loop [num 1000]
-        (when (> num 0)
-          (if-let [op (gen/generate (t.gen/gen-move-blocks-op @conn {:page-uuid page-uuid}))]
-            (do (outliner-op/apply-ops! test-helper/test-db-name-db-version conn
-                                        [op] "MMM do, yyyy" nil)
-                (recur (dec num)))
-            (recur (dec num)))))
-      (println "================ random moves ================")
-      (print-page-stat @conn page-uuid)
-      (undo-all conn page-uuid)
-      (print-page-stat @conn page-uuid)
-      (redo-all conn page-uuid)
-      (print-page-stat @conn page-uuid)
+        (undo-all! conn)
 
-      (loop [num 100]
-        (when (> num 0)
-          (if-let [op (gen/generate (t.gen/gen-delete-blocks-op @conn {:page-uuid page-uuid}))]
-            (do (outliner-op/apply-ops! test-helper/test-db-name-db-version conn
-                                        [op] "MMM do, yyyy" nil)
-                (recur (dec num)))
-            (recur (dec num)))))
-      (println "================ random deletes ================")
-      (print-page-stat @conn page-uuid)
-      (undo-all conn page-uuid)
-      (print-page-stat @conn page-uuid)
-      (try (redo-all conn page-uuid)
-           (catch :default e
-             (print-page-blocks-tree @conn page-uuid)
-             (throw e)))
-      (print-page-stat @conn page-uuid))
-    (catch :default e
-      (let [data (ex-data e)]
-        (fs-node/writeFileSync "debug.json" (sqlite-util/write-transit-str data))
-        (throw (js/Error "check debug.json"))))))
+        (is (= (get-datoms @conn) #{}))
 
-(comment
-  (deftest debug-test
-    (let [{:keys [origin-db db illegal-entity other]}
-          (dt/read-transit-str (str (fs-node/readFileSync "debug.json")))
-          _ (prn :illegal-entity illegal-entity :other other)
-          illegal-entity1 (d/entity origin-db illegal-entity)
-          illegal-entity-left1 (:block/left illegal-entity1)
-          illegal-entity-parent1 (:block/parent illegal-entity1)]
-      (prn "before transact"
-           (select-keys illegal-entity1 [:db/id :block/left :block/parent])
-           (select-keys illegal-entity-left1 [:db/id :block/left :block/parent])
-           (select-keys illegal-entity-parent1 [:db/id :block/left :block/parent]))
+        (redo-all! conn)
 
-      (let [illegal-entity2 (d/entity db illegal-entity)
-            illegal-entity-left2 (:block/left illegal-entity2)
-            illegal-entity-parent2 (:block/parent illegal-entity2)]
-        (prn "after transact"
-             (select-keys illegal-entity2 [:db/id :block/left :block/parent])
-             (select-keys illegal-entity-left2 [:db/id :block/left :block/parent])
-             (select-keys illegal-entity-parent2 [:db/id :block/left :block/parent]))))))
+        (is (= (get-datoms @conn) (get-datoms db-after)))))))