diff --git a/.github/workflows/deploy-and-branch.yml b/.github/workflows/deploy-and-branch.yml index 8e910c8eeb..6cd4406f8d 100644 --- a/.github/workflows/deploy-and-branch.yml +++ b/.github/workflows/deploy-and-branch.yml @@ -11,7 +11,7 @@ on: env: CLOJURE_VERSION: "1.11.1.1413" NODE_VERSION: "22" - JAVA_VERSION: "11" + JAVA_VERSION: "21" jobs: build-and-deploy: diff --git a/.github/workflows/deploy-db-test-pages.yml b/.github/workflows/deploy-db-test-pages.yml index 94436a6f3e..2c0583da86 100644 --- a/.github/workflows/deploy-db-test-pages.yml +++ b/.github/workflows/deploy-db-test-pages.yml @@ -7,7 +7,7 @@ on: env: CLOJURE_VERSION: "1.11.1.1413" NODE_VERSION: '24' - JAVA_VERSION: "11" + JAVA_VERSION: "21" jobs: build-and-deploy: diff --git a/.github/workflows/deploy-sync-test.yml b/.github/workflows/deploy-sync-test.yml index 4c63178a1e..16ec2ce7b7 100644 --- a/.github/workflows/deploy-sync-test.yml +++ b/.github/workflows/deploy-sync-test.yml @@ -11,7 +11,7 @@ on: env: CLOJURE_VERSION: "1.11.1.1413" NODE_VERSION: "22" - JAVA_VERSION: "11" + JAVA_VERSION: "21" jobs: build-and-deploy: diff --git a/deps/db/src/logseq/db.cljs b/deps/db/src/logseq/db.cljs index a1245d0009..7e3758709f 100644 --- a/deps/db/src/logseq/db.cljs +++ b/deps/db/src/logseq/db.cljs @@ -138,6 +138,7 @@ (f tx-report errors)) (let [debug-data (invalid-tx-debug-data tx-meta tx-data errors tx-report)] (prn :debug :invalid-data debug-data) + (prn :debug :errors errors) (throw (ex-info "DB write failed with invalid data" debug-data)))) (defn- transact-sync @@ -298,7 +299,12 @@ (batch-tx-fn conn) (batch-transact-cleanup! conn) (when-some [_storage (storage/storage @conn)] - (d/store @conn)) + (d/store @conn) + ;; batch-transact! bypasses conn/store-after-transact!, so keep tail bookkeeping in sync + ;; with the just-persisted root snapshot. + (swap! (:atom conn) assoc + :tx-tail [] + :db-last-stored @conn)) (let [batch-tx-data @*tx-data _ (reset! *tx-data nil) tx-report {:db-before db-before @@ -832,11 +838,9 @@ (d/q '[:find ?e ?a :in $ ?v :where - [?c :logseq.property.class/enable-bidirectional? ?c-enable?] - [(true? ?c-enable?)] - [?ea :logseq.property/classes ?c] + [?e ?a ?v] [?ea :db/ident ?a] - [?e ?a ?v]] + [?ea :logseq.property/classes ?c]] db v)) diff --git a/deps/db/src/logseq/db/frontend/validate.cljs b/deps/db/src/logseq/db/frontend/validate.cljs index 500c9de9df..26325f9207 100644 --- a/deps/db/src/logseq/db/frontend/validate.cljs +++ b/deps/db/src/logseq/db/frontend/validate.cljs @@ -21,35 +21,10 @@ [closed-schema?] (if closed-schema? closed-db-schema-explainer db-schema-explainer)) -(defn- block-uuid-immutability-errors - [{:keys [db-before db-after tx-data tx-meta]}] - (let [uuid-touched-existing-eids - (->> tx-data - (keep (fn [{:keys [e a]}] - (when (and (= :block/uuid a) - (number? e) - (some? (:block/uuid (d/entity db-before e)))) - e))) - distinct)] - (->> uuid-touched-existing-eids - (keep (fn [eid] - (let [before-uuid (:block/uuid (d/entity db-before eid)) - after-ent (d/entity db-after eid) - after-uuid (:block/uuid after-ent) - deleted? (nil? after-ent)] - (when (and (not deleted?) - (not= before-uuid after-uuid)) - {:entity-map {:db/id eid - :block/uuid before-uuid - :block/uuid-after after-uuid} - :errors {:block/uuid ["immutable for existing entities; use :db/retractEntity to delete entities"]} - :tx-meta tx-meta})))) - vec))) - (defn validate-tx-report "Validates the datascript tx-report for entities that have changed. Returns boolean indicating if db is valid" - [{:keys [db-after tx-data tx-meta] :as tx-report} {:keys [closed-schema?]}] + [{:keys [db-after tx-data tx-meta]} {:keys [closed-schema?]}] (binding [db-malli-schema/*skip-strict-url-validate?* true] (let [changed-ids (->> tx-data (keep :e) distinct) tx-datoms (mapcat (fn [id] @@ -85,8 +60,7 @@ (prn data))) data)) invalid-ent-maps)))) - uuid-errors (block-uuid-immutability-errors tx-report) - errors (->> (concat schema-errors uuid-errors) + errors (->> schema-errors (remove nil?) vec)] (if (seq errors) diff --git a/deps/db/test/logseq/db_test.cljs b/deps/db/test/logseq/db_test.cljs index fa66454ec6..b34aa38c0a 100644 --- a/deps/db/test/logseq/db_test.cljs +++ b/deps/db/test/logseq/db_test.cljs @@ -1,9 +1,21 @@ (ns logseq.db-test (:require [cljs.test :refer [deftest is testing]] [datascript.core :as d] + [datascript.storage :as ds-storage :refer [IStorage]] [logseq.db :as ldb] [logseq.db.test.helper :as db-test])) +(defrecord InMemoryStorage [*disk] + IStorage + (-store [_ addr+data-seq _delete-addrs] + (doseq [[addr data] addr+data-seq] + (vswap! *disk assoc addr data))) + (-restore [_ addr] + (get @*disk addr))) + +(defn- make-storage [] + (->InMemoryStorage (volatile! {}))) + ;;; datoms ;;; - 1 <----+ ;;; - 2 | @@ -122,33 +134,32 @@ :block/tags :logseq.class/Property}]) (ldb/transact! temp-conn [[:db/retract :logseq.class/Task :block/tags :logseq.class/Property]])))))) -(deftest block-uuid-is-immutable-for-existing-entity-test - (let [conn (db-test/create-conn-with-blocks - {:pages-and-blocks - [{:page {:block/title "page1"}}]}) - page (db-test/find-page-by-title @conn "page1") - db-id (:db/id page) - old-uuid (:block/uuid page) - new-uuid (random-uuid)] - (testing "cannot replace :block/uuid on existing entity" - (is (thrown? js/Error - (db-test/silence-stderr - (ldb/transact! conn [[:db/add db-id :block/uuid new-uuid]])))) - (is (= old-uuid (:block/uuid (d/entity @conn db-id))))) - (testing "cannot retract :block/uuid on existing entity" - (is (thrown? js/Error - (db-test/silence-stderr - (ldb/transact! conn [[:db/retract db-id :block/uuid old-uuid]])))) - (is (= old-uuid (:block/uuid (d/entity @conn db-id))))))) - -(deftest block-uuid-immutability-allows-retract-entity-test - (let [conn (db-test/create-conn-with-blocks - {:pages-and-blocks - [{:page {:block/title "page1"}}]}) - page (db-test/find-page-by-title @conn "page1") - db-id (:db/id page)] - (ldb/transact! conn [[:db/retractEntity db-id]]) - (is (nil? (d/entity @conn db-id))))) +(deftest test-batch-transact-clears-stale-tx-tail-before-next-store-tail + (let [block-uuid #uuid "00000001-2026-0421-0000-000000000000" + schema {:block/uuid {:db/unique :db.unique/identity}} + storage (make-storage) + conn (d/create-conn schema {:storage storage})] + ;; Valid history: unique value moves to another entity. + (d/transact! conn [[:db/add 28446 :block/uuid block-uuid]]) + (ldb/batch-transact! + conn + {} + (fn [batch-conn] + (d/transact! batch-conn [[:db/retract 28446 :block/uuid block-uuid]]) + (d/transact! batch-conn [[:db/add 28447 :block/uuid block-uuid]]))) + ;; Next tx uses tail-only persistence. + (d/transact! conn [[:db/add 1 :filler "x"]]) + (let [tail (get @(:*disk storage) @#'ds-storage/tail-addr) + stale? (some (fn [[e a v _tx]] + (and (= 28446 e) + (= :block/uuid a) + (= block-uuid v))) + (apply concat tail)) + restored (d/restore-conn storage)] + (is (nil? stale?) + "stale pre-batch datoms should not leak into tail after batch-transact!") + (is (= [28447] + (mapv :e (d/datoms @restored :avet :block/uuid block-uuid))))))) (deftest get-bidirectional-properties (testing "disabled by default" diff --git a/deps/outliner/src/logseq/outliner/op/construct.cljc b/deps/outliner/src/logseq/outliner/op/construct.cljc index 9583c82f74..5b89099a9d 100644 --- a/deps/outliner/src/logseq/outliner/op/construct.cljc +++ b/deps/outliner/src/logseq/outliner/op/construct.cljc @@ -61,25 +61,35 @@ x) :else x)) -(defn- stable-block-ref-with-tx-data +(defn- tx-data-block-uuid-ref + [tx-data entity-ref] + (some (fn [item] + (when (and (= entity-ref (:e item)) + (= :block/uuid (:a item)) + (uuid? (:v item))) + [:block/uuid (:v item)])) + tx-data)) + +(defn- stable-entity-ref-with-tx-data [db tx-data x] (let [entity-ref (stable-entity-ref db x)] (if (and (integer? entity-ref) (not (neg? entity-ref))) - (or (some (fn [item] - (when (and (= entity-ref (:e item)) - (= :block/uuid (:a item)) - (uuid? (:v item))) - [:block/uuid (:v item)])) - tx-data) + (or (tx-data-block-uuid-ref tx-data entity-ref) entity-ref) entity-ref))) +(defn- stable-block-ref-with-tx-data + [db tx-data x] + (stable-entity-ref-with-tx-data db tx-data x)) + (defn- sanitize-ref-value - [db v] - (cond - (vector? v) (stable-entity-ref db v) - (or (set? v) (sequential? v)) (set (map #(stable-entity-ref db %) v)) - :else (stable-entity-ref db v))) + ([db v] + (sanitize-ref-value db nil v)) + ([db tx-data v] + (cond + (vector? v) (stable-entity-ref-with-tx-data db tx-data v) + (or (set? v) (sequential? v)) (set (map #(stable-entity-ref-with-tx-data db tx-data %) v)) + :else (stable-entity-ref-with-tx-data db tx-data v)))) (defn- sanitize-upsert-property-schema [db schema] @@ -108,7 +118,7 @@ (defn- sanitize-block-payload ([db block] (sanitize-block-payload db block nil)) - ([db block {:keys [created-uuids]}] + ([db block {:keys [created-uuids tx-data]}] (if (map? block) (let [refs (sanitize-block-refs (:block/refs block)) created-ref-uuids (when (and (seq created-uuids) (seq refs)) @@ -123,7 +133,7 @@ (contains? transient-block-keys k) m (= "block.temp" (namespace k)) m (ref-attr? db k) - (assoc m k (sanitize-ref-value db v)) + (assoc m k (sanitize-ref-value db tx-data v)) :else (assoc m k v))) {} @@ -190,8 +200,8 @@ (dissoc block' rebase-refs-key rebase-created-refs-key))) (defn- sanitize-insert-block-payload - [db block] - (let [block' (sanitize-block-payload db block)] + [db tx-data block] + (let [block' (sanitize-block-payload db block {:tx-data tx-data})] (if (map? block') (dissoc block' :block/page :block/order rebase-refs-key) block'))) @@ -380,11 +390,56 @@ distinct vec)) +(defn- remap-lookup-ref-by-uuid-map + [uuid-map v] + (cond + (and (vector? v) + (= :block/uuid (first v)) + (uuid? (second v))) + [:block/uuid (get uuid-map (second v) (second v))] + + (set? v) + (into #{} (map #(remap-lookup-ref-by-uuid-map uuid-map %) v)) + + (sequential? v) + (into (empty v) (map #(remap-lookup-ref-by-uuid-map uuid-map %) v)) + + :else + v)) + +(defn- remap-block-lookup-values-by-uuid-map + [block uuid-map] + (if (map? block) + (reduce-kv (fn [m k v] + (assoc m k (remap-lookup-ref-by-uuid-map uuid-map v))) + {} + block) + block)) + +(defn- template-children-blocks-for-history + [db template-ref] + (when-let [template (d/entity db template-ref)] + (let [template-id (:db/id template) + template-blocks (some->> (ldb/get-block-and-children db (:block/uuid template) + {:include-property-block? true}) + rest + seq + vec)] + (when (seq template-blocks) + (vec + (cons (assoc (into {} (first template-blocks)) + :db/id (:db/id (first template-blocks)) + :logseq.property/used-template template-id) + (map (fn [block] + (assoc (into {} block) :db/id (:db/id block))) + (rest template-blocks)))))))) + (defn- canonicalize-insert-blocks-op [db tx-data args] (let [[blocks target-id opts] args created-uuids (created-block-uuids-from-tx-data tx-data) - blocks* (mapv #(sanitize-insert-block-payload db %) blocks) + source-blocks (mapv #(sanitize-insert-block-payload db tx-data %) blocks) + source-uuids (mapv :block/uuid source-blocks) target-ref (stable-entity-ref db target-id) target (d/entity db target-id) block-with-new-id (fn [block block-uuid] @@ -394,15 +449,25 @@ [:block/uuid (:block/uuid parent)]))) blocks* (if (seq created-uuids) (if (and (:replace-empty-target? opts) - (= (inc (count created-uuids)) (count blocks))) - (let [[fst-block & rst-blocks] blocks* + (= (inc (count created-uuids)) (count source-blocks))) + (let [[fst-block & rst-blocks] source-blocks created-rst-uuids created-uuids] (into [(assoc fst-block :block/uuid (:block/uuid target))] (if (seq created-rst-uuids) (map block-with-new-id rst-blocks created-rst-uuids) rst-blocks))) - (mapv block-with-new-id blocks* created-uuids)) - blocks)] + (mapv block-with-new-id source-blocks created-uuids)) + source-blocks) + uuid-remap (->> (map vector source-uuids (map :block/uuid blocks*)) + (keep (fn [[old-uuid new-uuid]] + (when (and (uuid? old-uuid) + (uuid? new-uuid) + (not= old-uuid new-uuid)) + [old-uuid new-uuid]))) + (into {})) + blocks* (if (seq uuid-remap) + (mapv #(remap-block-lookup-values-by-uuid-map % uuid-remap) blocks*) + blocks*)] [blocks* target-ref (assoc (dissoc (or opts {}) :outliner-op) @@ -438,7 +503,9 @@ :save-block (let [[block opts] args created-uuids (created-block-uuids-from-tx-data tx-data)] - [:save-block [(sanitize-block-payload db block {:created-uuids created-uuids}) opts]]) + [:save-block [(sanitize-block-payload db block {:created-uuids created-uuids + :tx-data tx-data}) + opts]]) :insert-blocks [:insert-blocks @@ -448,7 +515,8 @@ (let [[template-id target-id opts] args template-ref (stable-entity-ref db template-id) target-ref (stable-entity-ref db target-id) - template-blocks (:template-blocks opts) + template-blocks (or (some-> (:template-blocks opts) seq vec) + (template-children-blocks-for-history db template-ref)) opts-base (dissoc opts :template-id :outliner-op) opts' (if (seq template-blocks) (let [[blocks* _target-ref insert-opts] @@ -1121,9 +1189,11 @@ (unresolved-numeric-entity-id? root-id)) :apply-template - (let [[template-id target-id _opts] args] + (let [[template-id target-id opts] args + template-blocks (:template-blocks opts)] (or (unresolved-numeric-entity-id? template-id) - (unresolved-numeric-entity-id? target-id))) + (unresolved-numeric-entity-id? target-id) + (some #(numeric-id-in-block-ref-attrs? db %) template-blocks))) :recycle-delete-permanently (let [[root-id] args] @@ -1189,10 +1259,7 @@ (and (= :apply-template (:outliner-op tx-meta)) (:undo? tx-meta) (seq (:db-sync/inverse-outliner-ops tx-meta))) - (some-> (:db-sync/inverse-outliner-ops tx-meta) - seq - vec - (->> (mapv #(normalize-op-entry-ids db-before %)))) + explicit-inverse-outliner-ops (has-replace-empty-target-insert-op? forward-outliner-ops) built-inverse-outliner-ops diff --git a/deps/outliner/test/logseq/outliner/op_construct_test.cljs b/deps/outliner/test/logseq/outliner/op_construct_test.cljs index fd25875761..dbef8df174 100644 --- a/deps/outliner/test/logseq/outliner/op_construct_test.cljs +++ b/deps/outliner/test/logseq/outliner/op_construct_test.cljs @@ -195,6 +195,126 @@ (is (= target-uuid (get-in forward-outliner-ops [0 1 1])))))) +(deftest derive-history-outliner-ops-save-block-resolves-retracted-ref-id-from-db-before-test + (testing "save-block should canonicalize ref attrs using db-before when target ref is retracted in current tx" + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "page"} + :blocks [{:block/title "child"}]}]}) + child (db-test/find-block-by-content @conn "child") + child-uuid (:block/uuid child) + tag-uuid (random-uuid) + _ (d/transact! conn [{:db/id -1 + :block/uuid tag-uuid + :block/title "Tag" + :block/name "tag" + :block/tags :logseq.class/Tag}]) + tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid])) + tx-report (d/with @conn [[:db/retractEntity tag-id]] {}) + tx-meta {:outliner-op :save-block + :outliner-ops [[:save-block [{:block/uuid child-uuid + :block/tags #{tag-id}} + {}]]]} + {:keys [forward-outliner-ops]} + (op-construct/derive-history-outliner-ops + @conn (:db-after tx-report) (:tx-data tx-report) tx-meta)] + (is (= #{[:block/uuid tag-uuid]} + (get-in forward-outliner-ops [0 1 0 :block/tags])))))) + +(deftest derive-history-outliner-ops-insert-blocks-resolves-retracted-ref-id-from-tx-data-test + (testing "insert-blocks should canonicalize block ref attrs when ref entity is retracted in current tx" + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "page"} + :blocks [{:block/title "target"}]}]}) + target (db-test/find-block-by-content @conn "target") + target-id (:db/id target) + tag-uuid (random-uuid) + _ (d/transact! conn [{:db/id -1 + :block/uuid tag-uuid + :block/title "Tag" + :block/name "tag" + :block/tags :logseq.class/Tag}]) + tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid])) + tx-report (d/with @conn [[:db/retractEntity tag-id]] {}) + tx-meta {:outliner-op :insert-blocks + :outliner-ops [[:insert-blocks [[{:block/uuid (random-uuid) + :block/title "new child" + :block/tags #{tag-id}}] + target-id + {:sibling? true}]]]} + {:keys [forward-outliner-ops]} + (op-construct/derive-history-outliner-ops + @conn (:db-after tx-report) (:tx-data tx-report) tx-meta)] + (is (= #{[:block/uuid tag-uuid]} + (get-in forward-outliner-ops [0 1 0 0 :block/tags])))))) + +(deftest derive-history-outliner-ops-apply-template-undo-canonicalizes-template-block-refs-test + (testing "apply-template undo explicit inverse should canonicalize nested template-block ref attrs" + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "page"} + :blocks [{:block/title "template"} + {:block/title "target"}]}]}) + template (db-test/find-block-by-content @conn "template") + target (db-test/find-block-by-content @conn "target") + template-uuid (:block/uuid template) + target-uuid (:block/uuid target) + inserted-uuid (random-uuid) + tag-uuid (random-uuid) + _ (d/transact! conn [{:db/id -1 + :block/uuid tag-uuid + :block/title "Tag" + :block/name "tag" + :block/tags :logseq.class/Tag}]) + tag-id (:db/id (d/entity @conn [:block/uuid tag-uuid])) + tx-report (d/with @conn [[:db/retractEntity tag-id]] {}) + tx-meta {:outliner-op :apply-template + :undo? true + :db-sync/inverse-outliner-ops + [[:apply-template [template-uuid + target-uuid + {:template-blocks [{:block/uuid inserted-uuid + :block/title "inserted" + :block/tags #{tag-id}}] + :sibling? true}]]]} + {:keys [inverse-outliner-ops]} + (op-construct/derive-history-outliner-ops + @conn (:db-after tx-report) (:tx-data tx-report) tx-meta)] + (is (= #{[:block/uuid tag-uuid]} + (get-in inverse-outliner-ops [0 1 2 :template-blocks 0 :block/tags])))))) + +(deftest derive-history-outliner-ops-apply-template-captures-template-blocks-when-missing-in-op-test + (testing "apply-template forward op should capture concrete template-blocks even if original op omitted them" + (let [conn (db-test/create-conn-with-blocks + {:pages-and-blocks + [{:page {:block/title "page"} + :blocks [{:block/title "template" + :build/children [{:block/title "template child 1"} + {:block/title "template child 2"}]} + {:block/title "target"}]}]}) + template (db-test/find-block-by-content @conn "template") + template-child-1 (db-test/find-block-by-content @conn "template child 1") + template-child-2 (db-test/find-block-by-content @conn "template child 2") + target (db-test/find-block-by-content @conn "target") + inserted-child-1-uuid (random-uuid) + inserted-child-2-uuid (random-uuid) + tx-data [{:e 900001 :a :block/uuid :v inserted-child-1-uuid :added true} + {:e 900002 :a :block/uuid :v inserted-child-2-uuid :added true}] + _ (d/transact! conn [[:db/add (:db/id template-child-1) :block/refs (:db/id template-child-2)]]) + tx-meta {:outliner-op :apply-template + :outliner-ops [[:apply-template [(:db/id template) + (:db/id target) + {:sibling? true}]]]} + {:keys [forward-outliner-ops]} + (op-construct/derive-history-outliner-ops @conn @conn tx-data tx-meta)] + (is (= :apply-template (ffirst forward-outliner-ops))) + (is (= true (get-in forward-outliner-ops [0 1 2 :keep-uuid?]))) + (is (= [inserted-child-1-uuid inserted-child-2-uuid] + (mapv :block/uuid (get-in forward-outliner-ops [0 1 2 :template-blocks])))) + (is (= #{[:block/uuid inserted-child-2-uuid]} + (get-in forward-outliner-ops [0 1 2 :template-blocks 0 :block/refs])))))) + (deftest derive-history-outliner-ops-builds-delete-page-inverse-for-class-property-and-today-page-test (testing "delete-page inverse restores hard-retracted class/property/today pages with stable db/ident" (let [today (date-time-util/ms->journal-day (js/Date.)) diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 2d3be879b4..09ef019682 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -2007,7 +2007,8 @@ order-list-idx (:own-order-list-index config) page-title? (:page-title? config) collapsable? (editor-handler/collapsable? uuid {:semantic? true - :ignore-children? page-title?}) + :ignore-children? page-title? + :page-title? page-title?}) link? (boolean (:original-block config)) icon-size (if collapsed? 12 14) icon (icon-component/get-node-icon-cp block {:size icon-size :color? true :link? link?}) @@ -3436,7 +3437,8 @@ (when-not (:hide-title? config) [:div.block-main-container.flex.flex-row.gap-1 - {:style (when (:page-title? config) + {:class (when (:page-title? config) "is-page-title-row") + :style (when (:page-title? config) {:margin-left (cond (util/mobile?) 0 page-icon -36 diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css index afa188f2e8..62933023ca 100644 --- a/src/main/frontend/components/block.css +++ b/src/main/frontend/components/block.css @@ -250,8 +250,13 @@ } } -.ls-page-title .block-control-wrap { - height: initial; +.block-main-container.is-page-title-row > .block-control-wrap { + /* Center the page-title control against the page title's line box. + Mobile overrides --ls-page-title-size, so this stays aligned cross-platform. */ + --ls-page-title-line-height: 1.38; + --ls-block-control-size: 24px; + @apply relative; + top: calc((var(--ls-page-title-size, 32px) * var(--ls-page-title-line-height) - var(--ls-block-control-size)) / 2); } .block-title-wrap { diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index eef0beb95d..1074916840 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -3029,7 +3029,7 @@ true))) (defn- db-collapsable? - [block] + [block & {:keys [page-title?]}] (let [class-properties (:classes-properties (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))) db (db/get-db) attributes (set (remove #{:block/alias} db-property/db-attribute-properties)) @@ -3037,7 +3037,9 @@ (map (partial entity-plus/entity-memoized db)) (concat class-properties) (remove (fn [e] (attributes (:db/ident e)))) - (remove outliner-property/property-with-other-position?) + (remove (fn [k] + (when-not page-title? + (outliner-property/property-with-other-position? k)))) (remove (fn [e] (:logseq.property/hide? e))) (remove nil?))] (or (seq properties) @@ -3046,13 +3048,13 @@ (defn collapsable? ([block-id] (collapsable? block-id {})) - ([block-id {:keys [semantic? ignore-children?] + ([block-id {:keys [semantic? ignore-children? page-title?] :or {semantic? false ignore-children? false}}] (when block-id (if-let [block (db/entity [:block/uuid block-id])] (or (if ignore-children? false (db-model/has-children? block-id)) - (db-collapsable? block) + (db-collapsable? block {:page-title? page-title?}) (and (:outliner/block-title-collapse-enabled? (state/get-config)) (block-with-title? (get block :block/format :markdown) diff --git a/src/main/frontend/handler/worker.cljs b/src/main/frontend/handler/worker.cljs index 90c0ed3204..9f0e44a5c9 100644 --- a/src/main/frontend/handler/worker.cljs +++ b/src/main/frontend/handler/worker.cljs @@ -179,6 +179,23 @@ (defmethod handle :default [_ _worker data] (prn :debug "Worker data not handled: " data)) +(defn- report-worker-error! + [error-value] + (let [message (or (:message error-value) + (get error-value "message") + "Unexpected webworker error") + error-data (or (:data error-value) + (get error-value "data")) + cause-data (or (get-in error-value [:cause :data]) + (get-in error-value ["cause" "data"]))] + (state/pub-event! + [:capture-error + {:error (ex-info message (or (when (map? error-data) error-data) {})) + :payload {:worker-error? true} + :extra {:worker-error error-value + :worker-error-data error-data + :worker-cause-data cause-data}}]))) + (defn handle-message! [^js worker wrapped-worker] (assert worker "worker doesn't exists") @@ -192,8 +209,10 @@ ;; https://github.com/GoogleChromeLabs/comlink/blob/dffe9050f63b1b39f30213adeb1dd4b9ed7d2594/src/comlink.ts#L223-L236 (if (and (= "HANDLER" (.-type data)) (= "throw" (.-name data))) (if (.-isError (.-value ^js data)) - (do (js/console.error "Unexpected webworker error:" (-> data bean/->clj (get-in [:value :value]))) - (js/console.log (get-in (bean/->clj data) [:value :value :stack]))) + (let [error-value (-> data bean/->clj (get-in [:value :value]))] + (js/console.error "Unexpected webworker error:" error-value) + (js/console.log (:stack error-value)) + (report-worker-error! error-value)) (js/console.error "Unexpected webworker error :" data)) (if (string? data) (let [[e payload] (ldb/read-transit-str data)] diff --git a/src/main/frontend/worker/db_core.cljs b/src/main/frontend/worker/db_core.cljs index 2678425d38..a9e09dc9a7 100644 --- a/src/main/frontend/worker/db_core.cljs +++ b/src/main/frontend/worker/db_core.cljs @@ -400,7 +400,7 @@ :skip-validate-db? true})))) (defn- latest-remote-tx repo) conn (worker-state/get-datascript-conn repo)] (when (and conn (= local-tx remote-tx)) ; rebase @@ -491,9 +493,11 @@ (defn- history-action-error-reason [error] - (if (= "invalid rebase op" (ex-message error)) - :invalid-history-action-ops - :error)) + (let [message (ex-message error)] + (if (or (= "invalid rebase op" message) + (= "Non-transact outliner ops contain numeric entity ids" message)) + :invalid-history-action-ops + :error))) (defn- replay-entity-id-value [db v] diff --git a/src/main/frontend/worker/sync/client_op.cljs b/src/main/frontend/worker/sync/client_op.cljs index c972489713..17769d744f 100644 --- a/src/main/frontend/worker/sync/client_op.cljs +++ b/src/main/frontend/worker/sync/client_op.cljs @@ -230,10 +230,6 @@ " on conflict(key) do update set value = excluded.value") [(name k) (str v)])) -(defn- sqlite-delete-meta! - [db k] - (sqlite-run! db "delete from sync_meta where key = ?" [(name k)])) - (defn update-graph-uuid [repo graph-uuid] {:pre [(some? graph-uuid)]} @@ -245,12 +241,30 @@ (when-let [store (sqlite-store-or-throw repo)] (sqlite-get-meta store :graph-uuid))) +(defn get-local-tx + [repo] + (when-let [store (sqlite-store-or-throw repo)] + (when-let [result (sqlite-get-meta store :local-tx)] + (js/parseInt result 10)))) + (defn update-local-tx [repo t] - {:pre [(some? t)]} - (let [store (sqlite-store-or-throw repo)] + {:pre [(and (integer? t) (>= t 0))]} + (let [store (sqlite-store-or-throw repo) + prev-t (get-local-tx repo)] (assert (some? store) repo) + (when (and prev-t (< t prev-t)) + (throw (ex-info "local-tx should be monotonically increasing" + {:repo repo + :prev-t prev-t + :new-t t}))) (sqlite-set-meta! store :local-tx t))) + +(defn reset-local-tx + "Should be used only when uploading a graph" + [repo] + (update-local-tx repo 0)) + (defn update-local-checksum [repo checksum] {:pre [(some? checksum)]} @@ -258,17 +272,6 @@ (assert (some? store) repo) (sqlite-set-meta! store :db-sync/checksum checksum))) -(defn remove-local-tx - [repo] - (when-let [store (sqlite-store-or-throw repo)] - (sqlite-delete-meta! store :local-tx))) - -(defn get-local-tx - [repo] - (when-let [store (sqlite-store-or-throw repo)] - (let [raw (sqlite-get-meta store :local-tx)] - (some-> raw (js/parseInt 10))))) - (defn get-pending-local-tx-count [repo] (if-let [cached (get @*repo->pending-local-tx-count repo)] diff --git a/src/main/frontend/worker/sync/handle_message.cljs b/src/main/frontend/worker/sync/handle_message.cljs index 58a27ad76d..c83e8bd9e7 100644 --- a/src/main/frontend/worker/sync/handle_message.cljs +++ b/src/main/frontend/worker/sync/handle_message.cljs @@ -257,7 +257,7 @@ (defn- handle-tx-batch-ok! [repo client remote-tx remote-checksum] (require-non-negative remote-tx {:repo repo :type "tx/batch/ok"}) - (let [current-local-tx (or (client-op/get-local-tx repo) 0) + (let [current-local-tx (client-op/get-local-tx repo) next-local-tx (max current-local-tx remote-tx)] (client-op/update-local-tx repo next-local-tx) (sync-util/clear-last-sync-error! client) @@ -295,26 +295,16 @@ (declare handle-pull-ok! handle-changed!) -(defn- recover-from-remote-tx-reset! - [repo client local-tx remote-tx remote-checksum message-type] - (when (pending-local-tx? repo) - (fail-fast :db-sync/remote-tx-reset-with-pending-local - {:repo repo - :type message-type - :local-tx local-tx - :remote-tx remote-tx})) - (log/warn :db-sync/remote-tx-reset-detected - {:repo repo - :type message-type - :local-tx local-tx - :remote-tx remote-tx}) - (client-op/update-local-tx repo remote-tx) - (when (string? remote-checksum) - (client-op/update-local-checksum repo remote-checksum)) - (clear-pending-pull! client) - (request-pull! client remote-tx) - (broadcast-rtc-state! client)) - +(defn- validate-local-tx! + [repo message local-tx] + (let [message-type (:type message)] + (when (contains? #{"hello" "tx/batch/ok" "pull/ok" "changed" "tx/reject"} message-type) + (let [validate? (and (integer? local-tx) (>= local-tx 0))] + (when-not validate? + (throw (ex-info "Invalid local tx" + {:repo repo + :message-type message-type + :local-tx local-tx}))))))) (defn handle-message! [repo client raw] (let [message (-> raw @@ -322,26 +312,21 @@ sync-transport/coerce-ws-server-message)] (when-not (map? message) (fail-fast :db-sync/response-parse-failed {:repo repo :raw raw})) - (let [local-tx (or (client-op/get-local-tx repo) 0) + (let [local-tx (client-op/get-local-tx repo) remote-tx (:t message) - remote-checksum (:checksum message) - message-type (:type message)] + remote-checksum (:checksum message)] + (validate-local-tx! repo message local-tx) (update-latest-remote-state! repo message) - (if (and (contains? #{"hello" "changed"} message-type) - (number? local-tx) - (number? remote-tx) - (> local-tx remote-tx)) - (recover-from-remote-tx-reset! repo client local-tx remote-tx remote-checksum message-type) - (case message-type - "hello" (handle-hello! repo client local-tx remote-tx remote-checksum) - "online-users" (handle-online-users! repo client message) - "presence" (handle-presence! client message) - "tx/batch/ok" (handle-tx-batch-ok! repo client remote-tx remote-checksum) - "pull/ok" (handle-pull-ok! repo client local-tx remote-tx remote-checksum message) - "changed" (handle-changed! repo client local-tx remote-tx) - "tx/reject" (handle-tx-reject! repo client message local-tx) - (fail-fast :db-sync/invalid-field - {:repo repo :type message-type})))))) + (case (:type message) + "hello" (handle-hello! repo client local-tx remote-tx remote-checksum) + "online-users" (handle-online-users! repo client message) + "presence" (handle-presence! client message) + "tx/batch/ok" (handle-tx-batch-ok! repo client remote-tx remote-checksum) + "pull/ok" (handle-pull-ok! repo client local-tx remote-tx remote-checksum message) + "changed" (handle-changed! repo client local-tx remote-tx) + "tx/reject" (handle-tx-reject! repo client message local-tx) + (fail-fast :db-sync/invalid-field + {:repo repo :type (:type message)}))))) (defn- handle-pull-ok! [repo client local-tx remote-tx remote-checksum message] diff --git a/src/main/frontend/worker/sync/upload.cljs b/src/main/frontend/worker/sync/upload.cljs index acee0daddb..a95e467c47 100644 --- a/src/main/frontend/worker/sync/upload.cljs +++ b/src/main/frontend/worker/sync/upload.cljs @@ -266,8 +266,7 @@ (if (empty? rows) (do (sync-apply/clear-pending-txs! repo) - (client-op/remove-local-tx repo) - (client-op/update-local-tx repo 0) + (client-op/reset-local-tx repo) (client-op/add-all-exists-asset-as-ops repo) (update-progress {:sub-type :upload-completed :message "Graph upload finished!"}) diff --git a/src/resources/dicts/ja.edn b/src/resources/dicts/ja.edn index 4fc122f6b8..dc3298a141 100644 --- a/src/resources/dicts/ja.edn +++ b/src/resources/dicts/ja.edn @@ -970,7 +970,7 @@ :mobile.share/unsupported-content-warning "現在の共有コンテンツの解析はサポートされていません。以下のコードを{1}に報告してください。早急に対応いたします。" :mobile.share/unsupported-import-type "{1}ファイルのインポートはサポートされていません。{2}に報告してください。早急に対応いたします。" - :mobile.tab/capture "キャプチャ" + :mobile.tab/capture "メモ" :mobile.tab/go-to "移動" :mobile.tab/graphs "グラフ" diff --git a/src/resources/dicts/ko.edn b/src/resources/dicts/ko.edn index 8451131436..0b7fff880c 100644 --- a/src/resources/dicts/ko.edn +++ b/src/resources/dicts/ko.edn @@ -970,7 +970,7 @@ :mobile.share/unsupported-content-warning "현재 공유된 콘텐츠의 구문 분석은 지원되지 않습니다. {1}에 다음 코드를 보고해 주세요. 곧 확인하겠습니다." :mobile.share/unsupported-import-type "{1} 파일 가져오기는 지원되지 않습니다. {2}에 보고해 주시면 곧 확인하겠습니다." - :mobile.tab/capture "캡처" + :mobile.tab/capture "메모" :mobile.tab/go-to "이동" :mobile.tab/graphs "그래프" diff --git a/src/resources/dicts/ru.edn b/src/resources/dicts/ru.edn index a7958eac13..348fd9afc1 100644 --- a/src/resources/dicts/ru.edn +++ b/src/resources/dicts/ru.edn @@ -970,7 +970,7 @@ :mobile.share/unsupported-content-warning "Разбор текущего общего контента не поддерживается. Сообщите следующие коды на {1}. Мы скоро разберёмся." :mobile.share/unsupported-import-type "Импорт файлов {1} не поддерживается. Вы можете сообщить об этом на {2}. Мы скоро разберёмся." - :mobile.tab/capture "Захват" + :mobile.tab/capture "Сбор" :mobile.tab/go-to "Перейти" :mobile.tab/graphs "Графы" diff --git a/src/resources/dicts/uk.edn b/src/resources/dicts/uk.edn index fcd38e63b7..a3c93f6019 100644 --- a/src/resources/dicts/uk.edn +++ b/src/resources/dicts/uk.edn @@ -970,7 +970,7 @@ :mobile.share/unsupported-content-warning "Розбір поточного спільного вмісту не підтримується. Будь ласка, повідомте наступні коди на {1}. Ми розглянемо це найближчим часом." :mobile.share/unsupported-import-type "Імпорт файлу {1} не підтримується. Ви можете повідомити на {2}. Ми розглянемо це найближчим часом." - :mobile.tab/capture "Захват" + :mobile.tab/capture "Збір" :mobile.tab/go-to "Перейти" :mobile.tab/graphs "Графи" diff --git a/src/resources/dicts/zh-cn.edn b/src/resources/dicts/zh-cn.edn index d5f811daec..4697853c11 100644 --- a/src/resources/dicts/zh-cn.edn +++ b/src/resources/dicts/zh-cn.edn @@ -539,6 +539,10 @@ :electron/version "版本 {1}" :electron/write-file-error "写入文件 {1} 失败,{2}。" :electron/write-file-error-with-backup "写入文件 {1} 失败,{2}。备份文件已保存到 {3}。" + :electron/wrong-release-open-nightly "打开 Nightly 版本" + :electron/wrong-release-open-stable "下载正式版" + :electron/wrong-release-warning-detail "当前版本通过 Rosetta 运行,速度可能非常慢。请优先下载 Apple Silicon (arm64) 版本;若该版本暂不支持您当前的系统版本,可使用 Nightly 版本。" + :electron/wrong-release-warning-title "您正在 Apple Silicon 上使用 Intel (x64) 版本。" :encryption/cloud-password-rich (fn [] ["如果你丢失了密码,云端中的所有数据都将无法解密。" [:span "你仍然可以访问图谱的本地版本。"]]) :encryption/current-password "当前密码" @@ -970,7 +974,7 @@ :mobile.share/unsupported-content-warning "暂不支持解析当前分享的内容。请将以下代码反馈到 {1},我们会尽快处理。" :mobile.share/unsupported-import-type "不支持导入 {1} 文件。您可以在 {2} 上反馈,我们会尽快处理。" - :mobile.tab/capture "捕获" + :mobile.tab/capture "速记" :mobile.tab/go-to "跳转" :mobile.tab/graphs "图谱" diff --git a/src/resources/dicts/zh-hant.edn b/src/resources/dicts/zh-hant.edn index 0245bd111c..7bd1689a99 100644 --- a/src/resources/dicts/zh-hant.edn +++ b/src/resources/dicts/zh-hant.edn @@ -970,7 +970,7 @@ :mobile.share/unsupported-content-warning "目前不支援解析此分享內容。請在 {1} 上回報以下代碼,我們會盡快處理。" :mobile.share/unsupported-import-type "不支援匯入 {1} 檔案。您可以在 {2} 上回報,我們會盡快處理。" - :mobile.tab/capture "捕獲" + :mobile.tab/capture "速記" :mobile.tab/go-to "前往" :mobile.tab/graphs "圖譜" diff --git a/src/test/frontend/handler/worker_test.cljs b/src/test/frontend/handler/worker_test.cljs new file mode 100644 index 0000000000..d0040c0d1c --- /dev/null +++ b/src/test/frontend/handler/worker_test.cljs @@ -0,0 +1,32 @@ +(ns frontend.handler.worker-test + (:require [cljs.test :refer [deftest is]] + [frontend.handler.worker :as worker-handler] + [frontend.state :as state])) + +(deftest handle-message-reports-comlink-worker-throw-with-extra-data-test + (let [captured-events (atom []) + worker (js-obj) + worker-error {:message "Non-transact outliner ops contain numeric entity ids" + :data {:stage :forward-outliner-ops + :index 0} + :cause {:data {:op :save-block}} + :stack "Error: Non-transact outliner ops contain numeric entity ids"} + event #js {:data #js {:type "HANDLER" + :name "throw" + :value #js {:isError true + :value (clj->js worker-error)}}}] + (with-redefs [state/pub-event! (fn [payload] + (swap! captured-events conj payload))] + (worker-handler/handle-message! worker nil) + ((.-onmessage worker) event) + (is (= 1 (count @captured-events))) + (let [[event-type payload] (first @captured-events)] + (is (= :capture-error event-type)) + (is (= true (get-in payload [:payload :worker-error?]))) + (is (= {:stage "forward-outliner-ops" + :index 0} + (get-in payload [:extra :worker-error-data]))) + (is (= {:op "save-block"} + (get-in payload [:extra :worker-cause-data]))) + (is (= (:message worker-error) + (ex-message (:error payload)))))))) diff --git a/src/test/frontend/worker/db_sync_test.cljs b/src/test/frontend/worker/db_sync_test.cljs index 8ee0156e76..43d7df8f84 100644 --- a/src/test/frontend/worker/db_sync_test.cljs +++ b/src/test/frontend/worker/db_sync_test.cljs @@ -205,6 +205,10 @@ (swap! client-op/*repo->pending-local-tx-count dissoc test-repo) (reset! worker-state/*datascript-conns {test-repo db-conn}) (reset! worker-state/*client-ops-conns {test-repo ops-conn}) + (undo-redo/clear-history! test-repo) + ;; Keep sync-message tests deterministic under strict local-tx validation. + (when (and ops-conn (nil? (client-op/get-local-tx test-repo))) + (client-op/update-local-tx test-repo 0)) (when ops-conn (d/listen! db-conn ::listen-db (fn [tx-report] @@ -213,6 +217,7 @@ cleanup (fn [] (when ops-conn (d/unlisten! db-conn ::listen-db)) + (undo-redo/clear-history! test-repo) (swap! client-op/*repo->pending-local-tx-count dissoc test-repo) (reset! worker-state/*datascript-conns db-prev) (reset! worker-state/*client-ops-conns ops-prev))] @@ -1627,6 +1632,29 @@ (is (= before-title (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))) +(deftest apply-history-action-inline-semantic-op-rejects-numeric-ref-ids-test + (testing "inline semantic history action should reject stale numeric ref ids before replay" + (let [{:keys [conn client-ops-conn child1]} (setup-parent-child) + tx-id (random-uuid) + child-uuid (:block/uuid child1) + stale-ref-id 99999999] + (with-datascript-conns conn client-ops-conn + (fn [] + (let [result (#'sync-apply/apply-history-action! + test-repo + tx-id + false + {:outliner-op :save-block + :db-sync/forward-outliner-ops [[:save-block [{:block/uuid child-uuid + :block/tags #{stale-ref-id}} + {}]]] + :db-sync/inverse-outliner-ops [[:save-block [{:block/uuid child-uuid + :block/title "child 1"} + {}]]]})] + (is (= false (:applied? result))) + (is (= :invalid-history-action-ops + (:reason result))))))))) + (deftest apply-history-action-redo-invalid-insert-conflict-skips-fail-fast-test (testing "redo conflict on stale insert target should return error result without fail-fast logger" (let [{:keys [conn client-ops-conn]} (setup-parent-child) @@ -4690,6 +4718,43 @@ :template-blocks blocks-to-insert}]]] local-tx-meta))) +(defn- apply-template-with-opts! + [conn template-root-uuid target-uuid opts] + (let [template-root (d/entity @conn [:block/uuid template-root-uuid]) + target (d/entity @conn [:block/uuid target-uuid]) + template-blocks (->> (ldb/get-block-and-children @conn template-root-uuid + {:include-property-block? true}) + rest) + blocks-to-insert (cons (assoc (first template-blocks) + :logseq.property/used-template (:db/id template-root)) + (rest template-blocks))] + (apply-ops! + conn + [[:apply-template [(:db/id template-root) + (:db/id target) + (merge {:sibling? true + :template-blocks blocks-to-insert} + opts)]]] + local-tx-meta))) + +(defn- undo-all! + [repo] + (loop [n 0] + (let [result (undo-redo/undo repo)] + (when-not (= :frontend.worker.undo-redo/empty-undo-stack result) + (when (> n 128) + (throw (ex-info "undo loop exceeded" {:count n}))) + (recur (inc n)))))) + +(defn- redo-all! + [repo] + (loop [n 0] + (let [result (undo-redo/redo repo)] + (when-not (= :frontend.worker.undo-redo/empty-redo-stack result) + (when (> n 128) + (throw (ex-info "redo loop exceeded" {:count n}))) + (recur (inc n)))))) + (defn- select-offline-inserted-three [conn template-root-uuid] (let [all-three-ids (d/q '[:find [?b ...] @@ -4706,6 +4771,22 @@ all-threes) (first all-threes)))) +(defn- select-offline-inserted-one + [conn template-root-uuid] + (let [all-one-ids (d/q '[:find [?b ...] + :in $ ?title + :where + [?b :block/title ?title]] + @conn + "1") + all-ones (mapv #(d/entity @conn %) all-one-ids)] + (or (some (fn [b] + (when (not= template-root-uuid + (some-> b :block/parent :block/uuid)) + b)) + all-ones) + (first all-ones)))) + (defn- setup-rebase-apply-template-repro-state [] (let [template-root-uuid (random-uuid) @@ -4745,6 +4826,9 @@ :keep-uuid? true}]]] local-tx-meta) {:template-root-uuid template-root-uuid + :template-1-uuid template-1-uuid + :template-2-uuid template-2-uuid + :template-3-uuid template-3-uuid :empty-target-uuid empty-target-uuid :local-empty-uuid local-empty-uuid :seed-conn seed-conn @@ -4799,3 +4883,152 @@ (str (:errors validation)))))))) (finally (d/unlisten! conn-b ::capture-rebase-apply-template)))))) + +(deftest apply-history-action-redo-after-apply-template-undo-all-preserves-followup-insert-test + (testing "apply-template then insert-blocks should replay on redo after both are undone" + (let [{:keys [template-root-uuid empty-target-uuid seed-conn client-ops-conn]} + (setup-rebase-apply-template-repro-state) + conn (d/conn-from-db @seed-conn) + followup-uuid (random-uuid) + prev-apply-action @undo-redo/*apply-history-action!] + (with-datascript-conns conn client-ops-conn + (fn [] + (reset! undo-redo/*apply-history-action! sync-apply/apply-history-action!) + (try + (apply-template-to-empty-target! conn template-root-uuid empty-target-uuid) + (let [inserted-three (select-offline-inserted-three conn template-root-uuid)] + (is (some? inserted-three)) + (apply-ops! conn + [[:insert-blocks [[{:block/uuid followup-uuid + :block/title "followup"}] + (:db/id inserted-three) + {:sibling? true + :keep-uuid? true}]]] + local-tx-meta) + (undo-all! test-repo) + (is (nil? (d/entity @conn [:block/uuid followup-uuid]))) + (redo-all! test-repo) + (let [followup (d/entity @conn [:block/uuid followup-uuid])] + (is (some? followup)) + (is (= "followup" (:block/title followup))))) + (finally + (reset! undo-redo/*apply-history-action! prev-apply-action)))))))) + +(deftest apply-history-action-redo-after-non-empty-template-insert-preserves-followup-insert-test + (testing "non-empty apply-template + insert-blocks should replay on redo after undo-all" + (let [{:keys [template-root-uuid empty-target-uuid seed-conn client-ops-conn]} + (setup-rebase-apply-template-repro-state) + conn (d/conn-from-db @seed-conn) + followup-uuid (random-uuid) + prev-apply-action @undo-redo/*apply-history-action!] + (with-datascript-conns conn client-ops-conn + (fn [] + (reset! undo-redo/*apply-history-action! sync-apply/apply-history-action!) + (try + (d/transact! conn [[:db/add [:block/uuid empty-target-uuid] :block/title "target"]]) + (apply-template-with-opts! conn template-root-uuid empty-target-uuid {}) + (let [inserted-three (select-offline-inserted-three conn template-root-uuid)] + (is (some? inserted-three)) + (apply-ops! conn + [[:insert-blocks [[{:block/uuid followup-uuid + :block/title "followup"}] + (:db/id inserted-three) + {:sibling? true + :keep-uuid? true}]]] + local-tx-meta) + (undo-all! test-repo) + (is (nil? (d/entity @conn [:block/uuid followup-uuid]))) + (redo-all! test-repo) + (let [followup (d/entity @conn [:block/uuid followup-uuid])] + (is (some? followup)) + (is (= "followup" (:block/title followup))))) + (finally + (reset! undo-redo/*apply-history-action! prev-apply-action)))))))) + +(deftest undo-redo-apply-template-without-template-blocks-keeps-followup-insert-target-test + (testing "redo after apply-template (without template-blocks in original op) should preserve inserted target uuid" + (let [{:keys [template-root-uuid empty-target-uuid seed-conn client-ops-conn]} + (setup-rebase-apply-template-repro-state) + conn (d/conn-from-db @seed-conn) + followup-uuid (random-uuid) + prev-apply-action @undo-redo/*apply-history-action!] + (with-datascript-conns conn client-ops-conn + (fn [] + (reset! undo-redo/*apply-history-action! sync-apply/apply-history-action!) + (try + ;; Match editor path: apply-template op only carries target/template ids + opts. + (d/transact! conn [[:db/add [:block/uuid empty-target-uuid] :block/title "target"]]) + (apply-ops! conn + [[:apply-template [(:db/id (d/entity @conn [:block/uuid template-root-uuid])) + (:db/id (d/entity @conn [:block/uuid empty-target-uuid])) + {:sibling? true}]]] + local-tx-meta) + (let [inserted-three (select-offline-inserted-three conn template-root-uuid)] + (is (some? inserted-three)) + (apply-ops! conn + [[:insert-blocks [[{:block/uuid followup-uuid + :block/title "followup"}] + (:db/id inserted-three) + {:sibling? true + :keep-uuid? true}]]] + local-tx-meta) + (undo-all! test-repo) + (is (nil? (d/entity @conn [:block/uuid followup-uuid]))) + (redo-all! test-repo) + (let [followup (d/entity @conn [:block/uuid followup-uuid])] + (is (some? followup)) + (is (= "followup" (:block/title followup))))) + (finally + (reset! undo-redo/*apply-history-action! prev-apply-action)))))))) + +(deftest undo-redo-apply-template-without-template-blocks-rewrites-property-value-refs-test + (testing "redo apply-template should not keep property value refs to template source blocks" + (let [{:keys [template-root-uuid template-1-uuid template-3-uuid empty-target-uuid seed-conn client-ops-conn]} + (setup-rebase-apply-template-repro-state) + conn (d/conn-from-db @seed-conn) + prev-apply-action @undo-redo/*apply-history-action!] + (with-datascript-conns conn client-ops-conn + (fn [] + (reset! undo-redo/*apply-history-action! sync-apply/apply-history-action!) + (try + (d/transact! conn [[:db/add [:block/uuid template-1-uuid] :user.property/p1 [:block/uuid template-3-uuid]] + [:db/add [:block/uuid empty-target-uuid] :block/title "target"]]) + (apply-ops! conn + [[:apply-template [(:db/id (d/entity @conn [:block/uuid template-root-uuid])) + (:db/id (d/entity @conn [:block/uuid empty-target-uuid])) + {:sibling? true}]]] + local-tx-meta) + (let [inserted-one (select-offline-inserted-one conn template-root-uuid) + inserted-three (select-offline-inserted-three conn template-root-uuid) + property-value (:user.property/p1 inserted-one) + initial-ref-uuid (cond + (map? property-value) + (:block/uuid property-value) + + (and (vector? property-value) + (= :block/uuid (first property-value))) + (second property-value) + + :else + property-value)] + (is (= (:block/uuid inserted-three) initial-ref-uuid)) + (is (not= template-3-uuid initial-ref-uuid))) + (undo-all! test-repo) + (redo-all! test-repo) + (let [inserted-one (select-offline-inserted-one conn template-root-uuid) + inserted-three (select-offline-inserted-three conn template-root-uuid) + property-value (:user.property/p1 inserted-one) + redone-ref-uuid (cond + (map? property-value) + (:block/uuid property-value) + + (and (vector? property-value) + (= :block/uuid (first property-value))) + (second property-value) + + :else + property-value)] + (is (= (:block/uuid inserted-three) redone-ref-uuid)) + (is (not= template-3-uuid redone-ref-uuid))) + (finally + (reset! undo-redo/*apply-history-action! prev-apply-action))))))))