diff --git a/deps/db/src/logseq/db/common/normalize.cljs b/deps/db/src/logseq/db/common/normalize.cljs index f88af2f4fe..26633ff536 100644 --- a/deps/db/src/logseq/db/common/normalize.cljs +++ b/deps/db/src/logseq/db/common/normalize.cljs @@ -113,7 +113,7 @@ #{e block-uuid (str block-uuid)}) #{e})) -(defn- reorder-retract-entity +(defn reorder-retract-entity [tx-data] (let [retract-ops (filter retract-entity-op? tx-data) {recreated-block-retract-ops true @@ -122,7 +122,8 @@ (boolean (some (fn [x] (and - (= 5 (count x)) + (vector? x) + (>= (count x) 4) (= (first x) :db/add) (= (nth x 2) :block/uuid) (= (nth x 3) id))) tx-data))))) @@ -132,7 +133,8 @@ set) datom-for-retracted-eid? (fn [item] - (and (= 5 (count item)) + (and (vector? item) + (>= (count item) 4) (contains? retract-keys (second item)))) datoms-for-retracted-eids (filter datom-for-retracted-eid? tx-data) others (remove (fn [item] diff --git a/src/main/frontend/worker/sync/apply_txs.cljs b/src/main/frontend/worker/sync/apply_txs.cljs index 0eeabeb027..25f1fb252f 100644 --- a/src/main/frontend/worker/sync/apply_txs.cljs +++ b/src/main/frontend/worker/sync/apply_txs.cljs @@ -104,7 +104,8 @@ (let [reversed-datom (d/datom e a v t (not added))] ;; trick: reverse the order of `db-before` and `db-after` (db-normalize/normalize-datom db-before db-after reversed-datom)))) - (db-normalize/replace-attr-retract-with-retract-entity-v2 db-after))) + (db-normalize/replace-attr-retract-with-retract-entity-v2 db-after) + db-normalize/reorder-retract-entity)) (defn normalize-rebased-pending-tx [{:keys [db-before db-after tx-data]}] @@ -190,7 +191,8 @@ (if (and (vector? item) (= 5 (count item))) (let [[op e a v _t] item] [op e a v]) - item))))) + item))) + db-normalize/reorder-retract-entity)) (defn- inferred-outliner-ops? [tx-meta] @@ -307,7 +309,8 @@ (contains? op-construct/semantic-outliner-ops (first op-entry))))) seq) - tx-data (if undo? reversed-tx tx) + tx-data (-> (if undo? reversed-tx tx) + normalize-tx-data-for-rebase) ops' (if (seq ops) ops [[:transact [tx-data nil]]]) @@ -425,7 +428,8 @@ (defn- reverse-history-action! [conn local-tx] (if-let [tx-data (seq (:reversed-tx local-tx))] - (ldb/transact! conn tx-data + (ldb/transact! conn + (normalize-tx-data-for-rebase tx-data) {:outliner-op (:outliner-op local-tx) :reverse? true}) (invalid-rebase-op! :reverse-history-action diff --git a/src/test/frontend/worker/undo_redo_test.cljs b/src/test/frontend/worker/undo_redo_test.cljs index 3c33a180aa..d2c84b4761 100644 --- a/src/test/frontend/worker/undo_redo_test.cljs +++ b/src/test/frontend/worker/undo_redo_test.cljs @@ -159,6 +159,43 @@ (second %)) redo-op))) +(defn- move-retract-entity-ops-to-front + [tx-data] + (let [retract-entity-op? (fn [item] + (and (vector? item) + (= 2 (count item)) + (= :db/retractEntity (first item)))) + retract-ops (filter retract-entity-op? tx-data) + others (remove retract-entity-op? tx-data)] + (vec (concat retract-ops others)))) + +(defn- poison-history-tx-order! + [tx-id] + (when-let [entry (client-op/get-local-tx-entry test-repo tx-id)] + (client-op/upsert-local-tx-entry! + test-repo + {:tx-id tx-id + :pending? true + :failed? false + :outliner-op (:outliner-op entry) + :undo-redo (:db-sync/undo-redo entry) + :forward-outliner-ops (:forward-outliner-ops entry) + :inverse-outliner-ops (:inverse-outliner-ops entry) + :inferred-outliner-ops? (:inferred-outliner-ops? entry) + :normalized-tx-data (move-retract-entity-ops-to-front (:tx entry)) + :reversed-tx-data (move-retract-entity-ops-to-front (:reversed-tx entry))}))) + +(defn- property-value-titles + [value] + (cond + (nil? value) [] + (string? value) [value] + (map? value) [(:block/title value)] + (coll? value) (->> value + (mapcat property-value-titles) + vec) + :else [value])) + (deftest undo-missing-history-action-row-replays-from-inline-ops-test (testing "undo/redo should replay from inline history ops when pending row is missing" (worker-undo-redo/clear-history! test-repo) @@ -953,6 +990,40 @@ (worker-undo-redo/redo test-repo) (is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))) +(deftest repeated-set-block-property-text-value-undo-redo-test + (testing "set-block-property text value survives repeated undo/redo for one and many cardinalities" + (worker-undo-redo/clear-history! test-repo) + (let [conn (worker-state/get-datascript-conn test-repo) + {:keys [child-uuid]} (seed-page-parent-child!)] + (doseq [[suffix cardinality] [[:one :one] [:many :many]]] + (let [property-id (keyword (str "user.property/p1-undo-redo-" (name suffix)))] + (outliner-op/apply-ops! conn + [[:upsert-property [property-id + {:logseq.property/type :default + :db/cardinality cardinality} + {}]]] + (local-tx-meta {:client-id "test-client"})) + (worker-undo-redo/clear-history! test-repo) + (outliner-op/apply-ops! conn + [[:set-block-property [[:block/uuid child-uuid] + property-id + "value-1"]]] + (local-tx-meta {:client-id "test-client"})) + (let [history (latest-undo-history-data)] + (is (empty? (:db-sync/forward-outliner-ops history)))) + (dotimes [_ 3] + (when-let [undo-tx-id (:db-sync/tx-id (latest-undo-history-data))] + (poison-history-tx-order! undo-tx-id)) + (is (map? (worker-undo-redo/undo test-repo))) + (is (empty? (property-value-titles + (get (d/entity @conn [:block/uuid child-uuid]) property-id)))) + (when-let [redo-tx-id (:db-sync/tx-id (latest-redo-history-data))] + (poison-history-tx-order! redo-tx-id)) + (is (map? (worker-undo-redo/redo test-repo))) + (let [titles (property-value-titles + (get (d/entity @conn [:block/uuid child-uuid]) property-id))] + (is (contains? (set titles) "value-1"))))))))) + (deftest save-two-blocks-undo-targets-latest-block-test (testing "undo after saving two blocks reverts the latest saved block first" (worker-undo-redo/clear-history! test-repo)