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

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

Tienson Qin 1 год назад
Родитель
Сommit
356f45b8e5

+ 25 - 3
.github/workflows/build-desktop-release.yml

@@ -363,10 +363,20 @@ jobs:
         #  CODE_SIGN_CERTIFICATE_FILE: ../codesign.pfx
         #  CODE_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODE_SIGN_CERTIFICATE_PASSWORD }}
 
-      - name: Save Artifact
+      - name: Save Artifact for Code Signing
         run: |
           mkdir builds
           mv static\out\make\squirrel.windows\x64\*.exe    builds\Logseq-win-x64-${{ steps.ref.outputs.version }}.exe
+
+      - name: Upload Artifact for Code Signing
+        uses: actions/upload-artifact@v3
+        with:
+          name: logseq-win64-unsigned-builds
+          path: builds
+
+      - name: Save Artifact
+        run: |
+          rm builds\*.exe
           mv static\out\make\squirrel.windows\x64\*.nupkg  builds\Logseq-win-x64-${{ steps.ref.outputs.version }}-full.nupkg
           mv static\out\make\zip\win32\x64\*.zip           builds\Logseq-win-x64-${{ steps.ref.outputs.version }}.zip
           mv static\out\make\squirrel.windows\x64\RELEASES builds\RELEASES
@@ -551,7 +561,7 @@ jobs:
       - name: Download Windows Artifact
         uses: actions/download-artifact@v3
         with:
-          name: logseq-win64-builds
+          name: logseq-win64-unsigned-builds
           path: ./builds
 
       - name: Sign Windows Executable
@@ -602,6 +612,12 @@ jobs:
           name: logseq-win64-signed-builds
           path: ./
 
+      - name: Download The Windows Artifact
+        uses: actions/download-artifact@v3
+        with:
+          name: logseq-win64-builds
+          path: ./
+
       - name: Download Android Artifacts
         uses: actions/download-artifact@v3
         with:
@@ -669,12 +685,18 @@ jobs:
           name: logseq-linux-arm64-builds
           path: ./
 
-      - name: Download The Windows Artifact
+      - name: Download The Windows Artifact (Signed)
         uses: actions/download-artifact@v3
         with:
           name: logseq-win64-signed-builds
           path: ./
 
+      - name: Download The Windows Artifact
+        uses: actions/download-artifact@v3
+        with:
+          name: logseq-win64-builds
+          path: ./
+
       - name: Download Android Artifacts
         uses: actions/download-artifact@v3
         if: ${{ github.event_name == 'schedule' || github.event.inputs.build-android == 'true' }}

+ 28 - 0
deps/common/src/logseq/common/util.cljs

@@ -298,3 +298,31 @@
 (defn replace-first-ignore-case
   [s old-value new-value]
   (string/replace-first s (re-pattern (str "(?i)" (escape-regex-chars old-value))) new-value))
+
+
+(defn sort-coll-by-dependency
+  "Sort the elements in the collection based on dependencies.
+coll:  [{:id 1 :depend-on 2} {:id 2 :depend-on 3} {:id 3}]
+get-elem-id-fn: :id
+get-elem-dep-id-fn :depend-on
+return: [{:id 3} {:id 2 :depend-on 3} {:id 1 :depend-on 2}]"
+  [get-elem-id-fn get-elem-dep-id-fn coll]
+  (let [id->elem (into {} (keep (juxt get-elem-id-fn identity)) coll)
+        id->dep-id (into {} (keep (juxt get-elem-id-fn get-elem-dep-id-fn)) coll)
+        all-ids (set (keys id->dep-id))
+        sorted-ids
+        (loop [r []
+               rest-ids all-ids
+               id (first rest-ids)]
+          (if-not id
+            r
+            (if-let [dep-id (id->dep-id id)]
+              ;; TODO: check no dep-cycle
+              (if-let [next-id (get rest-ids dep-id)]
+                (recur r rest-ids next-id)
+                (let [rest-ids* (disj rest-ids id)]
+                  (recur (conj r id) rest-ids* (first rest-ids*))))
+              ;; not found dep-id, so this id can be put into result now
+              (let [rest-ids* (disj rest-ids id)]
+                (recur (conj r id) rest-ids* (first rest-ids*))))))]
+    (mapv id->elem sorted-ids)))

+ 7 - 5
deps/shui/src/logseq/shui/popup/core.cljs

@@ -100,9 +100,11 @@
 
 (defn hide!
   ([] (when-let [id (some-> (get-popups) (last) :id)] (hide! id 0)))
-  ([id] (hide! id 0))
-  ([id delay]
-   (let [f #(update-popup! id :open? false)]
+  ([id] (hide! id 0 {}))
+  ([id delay] (hide! id delay {}))
+  ([id delay {:keys [all?]}]
+   (let [f #(do (update-popup! id :open? false)
+                (when (true? all?) (update-popup! id :all? true)))]
      (if (and (number? delay) (> delay 0))
        (js/setTimeout f delay)
        (f)))))
@@ -110,7 +112,7 @@
 (defn hide-all!
   []
   (doseq [{:keys [id]} @*popups]
-    (hide! id)))
+    (hide! id 0 {:all? true})))
 
 (rum/defc x-popup
   [{:keys [id open? content position as-dropdown? as-content? force-popover?
@@ -195,5 +197,5 @@
 
     [:<>
      (for [config popups
-           :when (and (map? config) (:id config))]
+           :when (and (map? config) (:id config) (not (:all? config)))]
        (rum/with-key (x-popup config) (:id config)))]))

+ 3 - 0
docs/develop-logseq.md

@@ -68,6 +68,9 @@ The released files will be at `static/` directory.
 
 ``` bash
 yarn install
+cd static
+yarn install
+cd ..
 ```
 
 2. Compile to JavaScript and open the dev app

+ 1 - 1
package.json

@@ -44,7 +44,7 @@
         "dev-electron-app": "gulp electron",
         "release-electron": "run-s gulp:build && gulp electronMaker",
         "debug-electron": "cd static/ && yarn electron:debug",
-        "e2e-test": "cross-env CI=true npx playwright test --reporter github",
+        "e2e-test": "cross-env DEBUG=pw:api CI=true npx playwright test --reporter github",
         "run-android-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android && npx cap run android",
         "run-ios-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync ios && npx cap run ios",
         "clean": "gulp clean",

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

@@ -509,7 +509,7 @@
 (declare page-reference)
 
 (defn open-page-ref
-  [page-entity e page-name contents-page?]
+  [config page-entity e page-name contents-page?]
   (util/stop e)
   (when (not (util/right-click? e))
     (let [page (or (first (:block/_alias page-entity)) page-entity)]
@@ -530,7 +530,8 @@
         (state/pub-event! [:page/create page-name])
 
         :else
-        (route-handler/redirect-to-page! (:block/uuid page)))))
+        (-> (or (:on-redirect-to-page config) route-handler/redirect-to-page!)
+          (apply [(:block/uuid page)])))))
   (when (and contents-page?
              (util/mobile?)
              (state/get-left-sidebar-open?))
@@ -577,11 +578,11 @@
       :on-pointer-up (fn [e]
                        (when @*mouse-down?
                          (state/clear-edit!)
-                         (open-page-ref page-entity e page-name contents-page?)
+                         (open-page-ref config page-entity e page-name contents-page?)
                          (reset! *mouse-down? false)))
       :on-key-up (fn [e] (when (and e (= (.-key e) "Enter"))
                            (state/clear-edit!)
-                           (open-page-ref page-entity e page-name contents-page?)))}
+                           (open-page-ref config page-entity e page-name contents-page?)))}
      (when-not hide-icon?
        (when-let [icon (get page-entity (pu/get-pid :logseq.property/icon))]
          [:span.mr-1.inline-flex.items-center (icon/icon icon)]))
@@ -1727,7 +1728,7 @@
   (reset! *dragging-block block))
 
 (defn- bullet-on-click
-  [e block uuid]
+  [e block uuid {:keys [on-redirect-to-page]}]
   (cond
     (pu/shape-block? block)
     (route-handler/redirect-to-page! (get-in block [:block/page :block/uuid]) {:block-id uuid})
@@ -1747,7 +1748,9 @@
         (util/stop e))
 
     :else
-    (when uuid (route-handler/redirect-to-page! uuid))))
+    (when uuid
+      (-> (or on-redirect-to-page route-handler/redirect-to-page!)
+        (apply [(str uuid)])))))
 
 (declare block-list)
 (rum/defc block-children < rum/reactive
@@ -1818,7 +1821,7 @@
                          "control-hide")}
          (ui/rotating-arrow collapsed?)]])
 
-     (let [bullet [:a.bullet-link-wrap {:on-click #(bullet-on-click % block uuid)}
+     (let [bullet [:a.bullet-link-wrap {:on-click #(bullet-on-click % block uuid config)}
                    [:span.bullet-container.cursor
                     {:id (str "dot-" uuid)
                      :draggable true
@@ -2642,7 +2645,9 @@
                (if (:block/name block) :page :block)]))
 
            :else
-           (route-handler/redirect-to-page! (:block/uuid block))))}
+           (when-let [uuid (:block/uuid block)]
+             (-> (or (:on-redirect-to-page config) route-handler/redirect-to-page!)
+               (apply [(str uuid)])))))}
    label])
 
 (rum/defc breadcrumb-separator
@@ -2652,13 +2657,13 @@
 
 ;; "block-id - uuid of the target block of breadcrumb. page uuid is also acceptable"
 (rum/defc breadcrumb < rum/reactive
-  {:init (fn [state]
-           (let [args (:rum/args state)
-                 block-id (nth args 2)
-                 depth (:level-limit (last args))]
-             (p/let [id (:db/id (db/entity [:block/uuid block-id]))]
-               (when id (db-async/<get-block-parents (state/get-current-repo) id depth)))
-             state))}
+                       {:init (fn [state]
+                                (let [args (:rum/args state)
+                                      block-id (nth args 2)
+                                      depth (:level-limit (last args))]
+                                  (p/let [id (:db/id (db/entity [:block/uuid block-id]))]
+                                    (when id (db-async/<get-block-parents (state/get-current-repo) id depth)))
+                                  state))}
   [config repo block-id {:keys [show-page? indent? end-separator? level-limit _navigating-block]
                          :or {show-page? true
                               level-limit 3}

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

@@ -538,8 +538,8 @@
 
                    [:div
                     (when (and block? (not sidebar?) (not whiteboard?))
-                      (let [config {:id "block-parent"
-                                    :block? true}]
+                      (let [config (merge config {:id "block-parent"
+                                                  :block? true})]
                         [:div.mb-4
                          (component-block/breadcrumb config repo block-id {:level-limit 3})]))
 

+ 186 - 105
src/main/frontend/worker/undo_redo.cljs

@@ -1,11 +1,13 @@
 (ns frontend.worker.undo-redo
   "undo/redo related fns and op-schema"
-  (:require [datascript.core :as d]
+  (:require [clojure.set :as set]
+            [datascript.core :as d]
             [frontend.schema-register :include-macros true :as sr]
             [frontend.worker.batch-tx :include-macros true :as batch-tx]
             [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.outliner.core :as outliner-core]
             [logseq.outliner.transaction :as outliner-tx]
             [malli.core :as m]
@@ -26,9 +28,9 @@ so when undo, it will undo [<op0> <op1> <op2>] instead of [<op1> <op2>]")
   "boundary of one or more undo-ops.
 when one undo/redo will operate on all ops between two ::boundary")
 
-(sr/defkeyword ::insert-block
-  "when a block is inserted, generate a ::insert-block undo-op.
-when undo this op, the related block will be removed.")
+(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.")
@@ -53,10 +55,10 @@ when undo this op, this original entity-map will be transacted back into db")
    [:multi {:dispatch first}
     [::boundary
      [:cat :keyword]]
-    [::insert-block
+    [::insert-blocks
      [:cat :keyword
       [:map
-       [:block-uuid :uuid]]]]
+       [:block-uuids [:sequential :uuid]]]]]
     [::move-block
      [:cat :keyword
       [:map
@@ -82,6 +84,8 @@ when undo this op, this original entity-map will be transacted back into db")
       [:map
        [:block-uuid :uuid]
        [:block-origin-content {:optional true} :string]
+       [:block-origin-tags {:optional true} [:sequential :uuid]]
+       [:block-origin-collapsed {:optional true} :boolean]
        ;; TODO: add more attrs
        ]]]]))
 
@@ -110,32 +114,44 @@ when undo this op, this original entity-map will be transacted back into db")
       (seq (:block/tags m)) (update :block/tags (partial mapv :block/uuid)))))
 
 (defn- reverse-op
+  "return ops"
   [db op]
   (let [block-uuid (:block-uuid (second op))]
     (case (first op)
-      ::boundary op
+      ::boundary [op]
 
-      ::insert-block
-      [::remove-block
-       {:block-uuid block-uuid
-        :block-entity-map (->block-entity-map db [:block/uuid block-uuid])}]
+      ::insert-blocks
+      (mapv
+       (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))}])
+        [[::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-block {:block-uuid block-uuid}]
+      [[::insert-blocks {:block-uuids [block-uuid]}]]
 
       ::update-block
-      (let [block-origin-content (when (:block-origin-content (second op))
-                                   (:block/content (d/entity db [:block/uuid block-uuid])))]
-        [::update-block
-         (cond-> {:block-uuid block-uuid}
-           block-origin-content (assoc :block-origin-content block-origin-content))]))))
+      (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/content 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)))]
+        [[::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))]]))))
 
 (def ^:private apply-conj-vec (partial apply (fnil conj [])))
 
@@ -208,7 +224,8 @@ when undo this op, this original entity-map will be transacted back into db")
 
 (defn- normal-block?
   [entity]
-  (and (:block/parent entity)
+  (and (:block/uuid entity)
+       (:block/parent entity)
        (:block/left entity)))
 
 (defmulti ^:private reverse-apply-op (fn [op _conn _repo] (first op)))
@@ -236,27 +253,46 @@ when undo this op, this original entity-map will be transacted back into db")
                                              (assoc :block/updated-at (:block/updated-at block-entity-map))
 
                                              (seq (:block/tags block-entity-map))
-                                             (assoc :block/tags (mapv (partial vector :block/uuid)
-                                                                      (:block/tags block-entity-map))))]
+                                             (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}))
            (conj [:push-undo-redo])))))))
 
-(defmethod reverse-apply-op ::insert-block
+(defn- sort-block-entities
+  "return nil when there are other children existing"
+  [block-entities]
+  (let [sorted-block-entities (common-util/sort-coll-by-dependency
+                               :block/uuid (comp :block/uuid :block/parent) block-entities)
+        block-uuid-set (set (map :block/uuid sorted-block-entities))]
+    (when-not
+     (some ;; check no other children
+      (fn [ent]
+        (not-empty (set/difference (set (map :block/uuid (:block/_parent ent))) block-uuid-set)))
+      sorted-block-entities)
+
+      sorted-block-entities)))
+
+(defmethod reverse-apply-op ::insert-blocks
   [op conn repo]
-  (let [[_ {:keys [block-uuid]}] op]
-    (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
-      (when (empty? (:block/_parent block-entity)) ;if have children, skip
-        (some->>
-         (outliner-tx/transact!
-          {:gen-undo-op? false
-           :outliner-op :delete-blocks
-           :transact-opts {:repo repo
-                           :conn conn}}
-          (outliner-core/delete-blocks! repo conn
-                                        (common-config/get-date-formatter (worker-state/get-config repo))
-                                        [block-entity]
-                                        {:children? false}))
-         (conj [:push-undo-redo]))))))
+  (let [[_ {:keys [block-uuids]}] op]
+    (when-let [block-entities (->> block-uuids
+                                   (keep #(d/entity @conn [:block/uuid %]))
+                                   sort-block-entities
+                                   reverse
+                                   not-empty)]
+      (some->>
+       (outliner-tx/transact!
+        {:gen-undo-op? false
+         :outliner-op :delete-blocks
+         :transact-opts {:repo repo
+                         :conn conn}}
+        (outliner-core/delete-blocks! repo conn
+                                      (common-config/get-date-formatter (worker-state/get-config repo))
+                                      block-entities
+                                      {:children? false}))
+       (conj [:push-undo-redo])))))
 
 (defmethod reverse-apply-op ::move-block
   [op conn repo]
@@ -275,20 +311,49 @@ when undo this op, this original entity-map will be transacted back into db")
 
 (defmethod reverse-apply-op ::update-block
   [op conn repo]
-  (let [[_ {:keys [block-uuid block-origin-content]}] op]
+  (let [[_ {:keys [block-uuid block-origin-content block-origin-tags block-origin-collapsed]}] op]
     (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
       (when (normal-block? block-entity)
-        (let [new-block (assoc block-entity :block/content block-origin-content)]
-          (some->>
-           (outliner-tx/transact!
-            {:gen-undo-op? false
-             :outliner-op :save-block
-             :transact-opts {:repo repo
-                             :conn conn}}
-            (outliner-core/save-block! repo conn
-                                       (common-config/get-date-formatter (worker-state/get-config repo))
-                                       new-block))
-           (conj [:push-undo-redo])))))))
+        (let [db-id (:db/id block-entity)
+              _ (when (some? block-origin-tags)
+                  (d/transact! conn [[:db/retract db-id :block/tags]] {:gen-undo-op? false}))
+              new-block (cond-> block-entity
+                          (some? block-origin-content)
+                          (assoc :block/content 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)))
+              r2 (outliner-tx/transact!
+                  {:gen-undo-op? false
+                   :outliner-op :save-block
+                   :transact-opts {:repo repo
+                                   :conn conn}}
+                  (outliner-core/save-block! repo conn
+                                             (common-config/get-date-formatter (worker-state/get-config repo))
+                                             new-block))]
+
+          (when r2 [:push-undo-redo r2]))))))
+
+(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))]
+    (cond-> (concat sorted-remove-ops other-ops)
+      insert-op (conj insert-op))))
 
 (defn undo
   [repo page-block-uuid conn]
@@ -296,12 +361,12 @@ when undo this op, this original entity-map will be transacted back into db")
     (let [redo-ops-to-push (transient [])]
       (batch-tx/with-batch-tx-mode conn
         (doseq [op ops]
-          (let [rev-op (reverse-op @conn op)
+          (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)}))
-              (conj! redo-ops-to-push rev-op)))))
-      (when-let [rev-ops (not-empty (persistent! redo-ops-to-push))]
+              (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 (cons boundary rev-ops)))
       nil)
 
@@ -315,12 +380,12 @@ when undo this op, this original entity-map will be transacted back into db")
     (let [undo-ops-to-push (transient [])]
       (batch-tx/with-batch-tx-mode conn
         (doseq [op ops]
-          (let [rev-op (reverse-op @conn op)
+          (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)}))
-              (conj! undo-ops-to-push rev-op)))))
-      (when-let [rev-ops (not-empty (persistent! undo-ops-to-push))]
+              (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 (cons boundary rev-ops)))
       nil)
 
@@ -335,52 +400,66 @@ when undo this op, this original entity-map will be transacted back into db")
   (when-let [e (ffirst entity-datoms)]
     (let [attr->datom (id->attr->datom e)]
       (when (seq attr->datom)
-        (let [{[_ _ block-uuid _ add1?]    :block/uuid
-               [_ _ block-content _ add2?] :block/content
+        (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)]
-          (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-block {:block-uuid (: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))]
-              (cond-> []
-                origin-left-and-parent-available-in-db-after?
-                (conj [::move-block
-                       {:block-uuid (:block/uuid entity-after)
-                        :block-origin-left (:block/uuid (:block/left entity-before))
-                        :block-origin-parent (:block/uuid (:block/parent entity-before))}])
-
-                (and add2? block-content)
-                (conj [::update-block
-                       {:block-uuid (:block/uuid entity-after)
-                        :block-origin-content (:block/content entity-before)}])))
-
-            (and add2? block-content
-                 (normal-block? entity-after))
-            [[::update-block
-              {:block-uuid (:block/uuid entity-after)
-               :block-origin-content (:block/content entity-before)}]]))))))
+              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/content :block/tags :block/collapsed?}))]
+                (when-let [update-block-op-value
+                           (when (normal-block? entity-after)
+                             (some->> updated-attrs
+                                      (keep
+                                       (fn [attr-name]
+                                         (case attr-name
+                                           :block/content
+                                           (when-let [origin-content (:block/content 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))]
+
+                                           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]
@@ -394,7 +473,8 @@ when undo this op, this original entity-map will be transacted back into db")
 (defn- generate-undo-ops
   [repo db-before db-after same-entity-datoms-coll id->attr->datom gen-boundary-op?]
   (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)]
+    (let [ops (mapcat (partial entity-datoms=>ops db-before db-after id->attr->datom) same-entity-datoms-coll)
+          ops (sort&merge-ops ops)]
       (when (seq ops)
         (push-undo-ops repo page-block-uuid (if gen-boundary-op? (cons boundary ops) ops))))))
 
@@ -415,19 +495,20 @@ when undo this op, this original entity-map will be transacted back into db")
 (comment
 
   (clear-undo-redo-stack)
-  (add-watch (:undo/repo->undo-stack @worker-state/*state)
+  (add-watch (:undo/repo->pege-block-uuid->undo-ops @worker-state/*state)
              :xxx
              (fn [_ _ o n]
                (cljs.pprint/pprint {:k :undo
                                     :o o
                                     :n n})))
 
-  (add-watch (:undo/repo->redo-stack @worker-state/*state)
+  (add-watch (:undo/repo->pege-block-uuid->redo-ops @worker-state/*state)
              :xxx
              (fn [_ _ o n]
                (cljs.pprint/pprint {:k :redo
                                     :o o
                                     :n n})))
 
-  (remove-watch (:undo/repo->undo-stack @worker-state/*state) :xxx)
-  (remove-watch (:undo/repo->redo-stack @worker-state/*state) :xxx))
+  (remove-watch (:undo/repo->pege-block-uuid->undo-ops @worker-state/*state) :xxx)
+  (remove-watch (:undo/repo->pege-block-uuid->redo-ops @worker-state/*state) :xxx)
+  )

+ 2 - 13
src/main/logseq/sdk/experiments.cljs

@@ -2,25 +2,14 @@
   (:require [frontend.state :as state]
             [frontend.components.page :as page]
             [frontend.util :as util]
+            [logseq.sdk.utils :as sdk-util]
             [camel-snake-kebab.core :as csk]
             [goog.object :as gobj]
             [frontend.handler.plugin :as plugin-handler]))
 
-(defn- jsx->clj
-  [^js obj]
-  (if (js/goog.isObject obj)
-    (-> (fn [result k]
-          (let [v (gobj/get obj k)
-                k (keyword (csk/->kebab-case k))]
-            (if (= "function" (goog/typeOf v))
-              (assoc result k v)
-              (assoc result k (jsx->clj v)))))
-      (reduce {} (gobj/getKeys obj)))
-    obj))
-
 (defn ^:export cp_page_editor
   [^js props]
-  (let [props1 (jsx->clj props)
+  (let [props1 (sdk-util/jsx->clj props)
         page-name (some-> props1 :page)
         linked-refs? (some-> props1 :include-linked-refs)
         unlinked-refs? (some-> props1 :include-unlinked-refs)

+ 15 - 0
src/main/logseq/sdk/utils.cljs

@@ -2,6 +2,7 @@
   (:require [clojure.walk :as walk]
             [camel-snake-kebab.core :as csk]
             [frontend.util :as util]
+            [goog.object :as gobj]
             [cljs-bean.core :as bean]))
 
 (defn normalize-keyword-for-json
@@ -31,6 +32,20 @@
     :else
     (throw (js/Error. (str s " is not a valid UUID string.")))))
 
+(defn jsx->clj
+  [^js obj]
+  (if (js/goog.isObject obj)
+    (-> (fn [result k]
+          (let [v (gobj/get obj k)
+                k (keyword (csk/->kebab-case k))]
+            (if (= "function" (goog/typeOf v))
+              (assoc result k v)
+              (assoc result k (jsx->clj v)))))
+      (reduce {} (gobj/getKeys obj)))
+    obj))
+
 (def ^:export to-clj bean/->clj)
+(def ^:export jsx-to-clj jsx->clj)
+(def ^:export to-js bean/->js)
 (def ^:export to-keyword keyword)
 (def ^:export to-symbol symbol)

+ 30 - 10
src/test/frontend/worker/undo_redo_test.cljs

@@ -42,8 +42,8 @@
 (defn- gen-insert-block-op
   [db]
   (gen/let [block-uuid (gen-block-uuid db)]
-    [:frontend.worker.undo-redo/insert-block
-     {:block-uuid block-uuid}]))
+    [:frontend.worker.undo-redo/insert-blocks
+     {:block-uuids [block-uuid]}]))
 
 (defn- gen-remove-block-op
   [db]
@@ -60,11 +60,16 @@
 
 (defn- gen-update-block-op
   [db]
-  (gen/let [block-uuid (gen-block-uuid db)
-            content gen/string-alphanumeric]
-    [:frontend.worker.undo-redo/update-block
-     {:block-uuid block-uuid
-      :block-origin-content content}]))
+  (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]))
 
@@ -107,9 +112,11 @@
     (assert (some? (d/entity current-db [:block/uuid (:block-uuid (second op))]))
             {:op op :tx-data (:tx-data tx)})
 
-    :frontend.worker.undo-redo/insert-block
-    (assert (nil? (d/entity current-db [:block/uuid (:block-uuid (second op))]))
-            {:op op :tx-data (:tx-data tx) :x (keys tx)})
+    :frontend.worker.undo-redo/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)}))
+
     :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)})
@@ -169,3 +176,16 @@
 
         (is (= origin-graph-block-set (get-db-block-set @conn)))))
     ))
+
+;;; TODO: generate outliner-ops then undo/redo/validate
+;; (deftest undo-redo-single-step-check-gen-test
+;;   (let [conn (db/get-db false)
+;;         all-remove-ops (gen/generate (gen/vector (gen-op @conn {:remove-block-op 1000}) 20))]
+;;     (#'undo-redo/push-undo-ops test-helper/test-db-name-db-version page-uuid all-remove-ops)
+;;     (loop []
+;;       (when (not= :frontend.worker.undo-redo/empty-undo-stack
+;;                   (undo-redo/undo test-helper/test-db-name-db-version page-uuid conn))
+;;         (recur)))
+;;     (prn :init-blocks (d/entity @conn ))
+
+;;     ))