diff --git a/deps/outliner/src/logseq/outliner/op/construct.cljc b/deps/outliner/src/logseq/outliner/op/construct.cljc index 97125c9ef3..a30b2aa723 100644 --- a/deps/outliner/src/logseq/outliner/op/construct.cljc +++ b/deps/outliner/src/logseq/outliner/op/construct.cljc @@ -390,6 +390,24 @@ distinct vec)) +(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 @@ -460,7 +478,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] diff --git a/deps/outliner/test/logseq/outliner/op_construct_test.cljs b/deps/outliner/test/logseq/outliner/op_construct_test.cljs index 53357b0ae6..9585b1d9c4 100644 --- a/deps/outliner/test/logseq/outliner/op_construct_test.cljs +++ b/deps/outliner/test/logseq/outliner/op_construct_test.cljs @@ -284,6 +284,32 @@ (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") + 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}] + 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]))))))) + (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/test/frontend/worker/db_sync_test.cljs b/src/test/frontend/worker/db_sync_test.cljs index c2db07bc1b..1e507f4a97 100644 --- a/src/test/frontend/worker/db_sync_test.cljs +++ b/src/test/frontend/worker/db_sync_test.cljs @@ -4881,3 +4881,39 @@ (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))))))))