mirror of
https://github.com/logseq/logseq.git
synced 2026-05-21 19:24:17 +00:00
enhance(recycle): permanently delete recycled roots with sync-safe replay
This commit is contained in:
10
deps/outliner/src/logseq/outliner/op.cljs
vendored
10
deps/outliner/src/logseq/outliner/op.cljs
vendored
@@ -8,6 +8,7 @@
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.page :as outliner-page]
|
||||
[logseq.outliner.property :as outliner-property]
|
||||
[logseq.outliner.recycle :as outliner-recycle]
|
||||
[logseq.outliner.transaction :as outliner-tx]
|
||||
[malli.core :as m]))
|
||||
|
||||
@@ -128,6 +129,11 @@
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid ::option]]]]
|
||||
|
||||
[:recycle-delete-permanently
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
[:args [:tuple ::uuid]]]]
|
||||
|
||||
[:toggle-reaction
|
||||
[:catn
|
||||
[:op :keyword]
|
||||
@@ -346,6 +352,10 @@
|
||||
(let [[page-uuid opts] args]
|
||||
(outliner-page/delete! conn page-uuid (merge opts opts')))
|
||||
|
||||
:recycle-delete-permanently
|
||||
(let [[root-uuid] args]
|
||||
(outliner-recycle/permanently-delete! conn root-uuid))
|
||||
|
||||
:toggle-reaction
|
||||
(reset! *result (apply toggle-reaction! conn args))
|
||||
nil))
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
:rename-page
|
||||
:delete-page
|
||||
:restore-recycled
|
||||
:recycle-delete-permanently
|
||||
:set-block-property
|
||||
:remove-block-property
|
||||
:batch-set-property
|
||||
@@ -441,6 +442,10 @@
|
||||
(let [[root-id] args]
|
||||
[:restore-recycled [root-id]])
|
||||
|
||||
:recycle-delete-permanently
|
||||
(let [[root-id] args]
|
||||
[:recycle-delete-permanently [(stable-entity-ref db root-id)]])
|
||||
|
||||
:set-block-property
|
||||
(let [[block-eid property-id v] args]
|
||||
[:set-block-property [(stable-entity-ref db block-eid)
|
||||
|
||||
20
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
20
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
@@ -229,6 +229,26 @@
|
||||
(ldb/transact! conn tx-data {:outliner-op :restore-recycled})
|
||||
true)))
|
||||
|
||||
(defn ^:api permanently-delete-tx-data
|
||||
[db root]
|
||||
(when (and root (recycled? root))
|
||||
(->> (if (ldb/page? root)
|
||||
(keep (fn [id]
|
||||
(some-> (d/entity db id) :block/uuid))
|
||||
(page-tree-ids db root))
|
||||
(keep :block/uuid (block-subtree db root)))
|
||||
(map (fn [block-uuid]
|
||||
[:db/retractEntity [:block/uuid block-uuid]]))
|
||||
distinct
|
||||
seq)))
|
||||
|
||||
(defn ^:api permanently-delete!
|
||||
[conn root-uuid]
|
||||
(when-let [root (d/entity @conn [:block/uuid root-uuid])]
|
||||
(when-let [tx-data (permanently-delete-tx-data @conn root)]
|
||||
(ldb/transact! conn tx-data {:outliner-op :recycle-delete-permanently})
|
||||
true)))
|
||||
|
||||
(defn- gc-tx-data
|
||||
[db {:keys [now-ms] :or {now-ms (common-util/time-ms)}}]
|
||||
(let [cutoff (- now-ms retention-ms)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns logseq.outliner.recycle-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.recycle :as recycle]))
|
||||
@@ -16,3 +17,34 @@
|
||||
(is (nil? (:block/parent page')))
|
||||
(is (nil? (:logseq.property/deleted-at page')))
|
||||
(is (nil? (:logseq.property.recycle/original-parent page'))))))
|
||||
|
||||
(deftest permanently-delete-recycled-page-removes-page-and-descendants
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "b1"}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
block (db-test/find-block-by-content @conn "b1")
|
||||
page-uuid (:block/uuid page)
|
||||
block-uuid (:block/uuid block)]
|
||||
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn page-uuid)))
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid block-uuid])))))
|
||||
|
||||
(deftest permanently-delete-recycled-block-removes-subtree-only
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page1"}
|
||||
:blocks [{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}])
|
||||
page (ldb/get-page @conn "page1")
|
||||
parent (db-test/find-block-by-content @conn "parent")
|
||||
child (db-test/find-block-by-content @conn "child")
|
||||
parent-uuid (:block/uuid parent)
|
||||
child-uuid (:block/uuid child)]
|
||||
(ldb/transact! conn (recycle/recycle-blocks-tx-data @conn [parent] {}) {:outliner-op :delete-blocks})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid parent-uuid]))))
|
||||
(is (true? (recycle/permanently-delete! conn parent-uuid)))
|
||||
(is (some? (d/entity @conn [:block/uuid (:block/uuid page)])))
|
||||
(is (nil? (d/entity @conn [:block/uuid parent-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid])))))
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
[datascript.core :as d]
|
||||
[frontend.components.block :as component-block]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.react :as react]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.page :as page-handler]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.ui :as shui]
|
||||
[rum.core :as rum]))
|
||||
@@ -36,6 +39,20 @@
|
||||
(map #(d/entity db %))
|
||||
(sort-by :logseq.property/deleted-at #(compare %2 %1))))
|
||||
|
||||
(defn- sub-deleted-root-ids
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(some-> (react/q repo
|
||||
[:frontend.worker.react/recycle-roots]
|
||||
{:query-fn (fn [db _]
|
||||
(->> (d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :logseq.property/deleted-at]]
|
||||
db)
|
||||
vec))}
|
||||
nil)
|
||||
util/react)))
|
||||
|
||||
(defn- group-title
|
||||
[db root]
|
||||
(if (ldb/page? root)
|
||||
@@ -59,7 +76,11 @@
|
||||
(defn- deleted-root-header
|
||||
[db root]
|
||||
(let [user (deleted-by db root)
|
||||
deleted-at (:logseq.property/deleted-at root)]
|
||||
deleted-at (:logseq.property/deleted-at root)
|
||||
root-uuid (:block/uuid root)
|
||||
delete-message (str "Permanently delete this "
|
||||
(if (ldb/page? root) "page" "block")
|
||||
" from Recycle? This cannot be undone.")]
|
||||
[:div.flex.items-center.justify-between.gap-4.text-xs.text-muted-foreground
|
||||
[:div.flex.items-center.gap-1.min-w-0.flex-1
|
||||
(deleted-by-avatar user)
|
||||
@@ -68,12 +89,20 @@
|
||||
(str (if (ldb/page? root) "Page" "Block")
|
||||
" deleted "
|
||||
(.toLocaleString (js/Date. deleted-at)))]]]
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4"
|
||||
:on-click #(page-handler/restore-recycled! (:block/uuid root))}
|
||||
"Restore")]))
|
||||
[:div.flex.items-center.gap-1
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4"
|
||||
:on-click #(page-handler/restore-recycled! root-uuid)}
|
||||
"Restore")
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4 hover:text-destructive"
|
||||
:on-click #(when (js/confirm delete-message)
|
||||
(page-handler/delete-recycled-permanently! root-uuid))}
|
||||
"Delete permanently")]]))
|
||||
|
||||
(defn- deleted-root-outliner
|
||||
[root]
|
||||
@@ -88,10 +117,17 @@
|
||||
:id (str (:block/uuid root))}
|
||||
root))
|
||||
|
||||
(rum/defc recycle-page
|
||||
(rum/defc recycle-page < rum/reactive db-mixins/query
|
||||
[_page]
|
||||
(let [db* (db/get-db)
|
||||
groups (->> (deleted-roots db*)
|
||||
root-ids (or (sub-deleted-root-ids)
|
||||
[])
|
||||
roots (if (seq root-ids)
|
||||
(->> root-ids
|
||||
(keep #(d/entity db* %))
|
||||
(sort-by :logseq.property/deleted-at #(compare %2 %1)))
|
||||
(deleted-roots db*))
|
||||
groups (->> roots
|
||||
(group-by #(group-title db* %))
|
||||
(sort-by (fn [[_ roots]]
|
||||
(:logseq.property/deleted-at (first roots)))
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
(outliner-op/transact! tx-data nil))
|
||||
true))))
|
||||
|
||||
(defn delete-recycled-permanently!
|
||||
[root-uuid]
|
||||
(when-let [root (db/entity [:block/uuid root-uuid])]
|
||||
(when-let [tx-data (seq (outliner-recycle/permanently-delete-tx-data (db/get-db) root))]
|
||||
(p/do!
|
||||
(ui-outliner-tx/transact!
|
||||
{:outliner-op :recycle-delete-permanently}
|
||||
(outliner-op/recycle-delete-permanently! root-uuid))
|
||||
true))))
|
||||
|
||||
(defn <unfavorite-page!
|
||||
[page-name]
|
||||
(p/do!
|
||||
|
||||
@@ -163,3 +163,8 @@
|
||||
([page-uuid opts]
|
||||
(op-transact!
|
||||
[:delete-page [page-uuid (current-user-delete-opts opts)]])))
|
||||
|
||||
(defn recycle-delete-permanently!
|
||||
[root-uuid]
|
||||
(op-transact!
|
||||
[:recycle-delete-permanently [root-uuid]]))
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
(s/def ::objects (s/tuple #(= ::objects %) int?))
|
||||
;; get block reactions
|
||||
(s/def ::block-reactions (s/tuple #(= ::block-reactions %) int?))
|
||||
;; recycle roots list
|
||||
(s/def ::recycle-roots (s/tuple #(= ::recycle-roots %)))
|
||||
;; custom react-query
|
||||
(s/def ::custom any?)
|
||||
|
||||
@@ -27,6 +29,7 @@
|
||||
:refs ::refs
|
||||
:objects ::objects
|
||||
:block-reactions ::block-reactions
|
||||
:recycle-roots ::recycle-roots
|
||||
:custom ::custom))
|
||||
|
||||
(s/def ::affected-keys (s/coll-of ::react-query-keys))
|
||||
@@ -71,6 +74,9 @@
|
||||
(= :logseq.property.reaction/target (:a datom))) tx-data)
|
||||
(map :v)
|
||||
(distinct))
|
||||
recycle-roots? (some (fn [datom]
|
||||
(= :logseq.property/deleted-at (:a datom)))
|
||||
tx-data)
|
||||
other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
|
||||
(map :e))
|
||||
blocks (-> (concat blocks other-blocks) distinct)
|
||||
@@ -114,6 +120,9 @@
|
||||
(when tag [::objects tag]))
|
||||
tags)
|
||||
|
||||
(when recycle-roots?
|
||||
[[::recycle-roots]])
|
||||
|
||||
(when journals?
|
||||
[[::journals]]))]
|
||||
(->>
|
||||
|
||||
@@ -800,6 +800,27 @@
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :restore-recycled}))
|
||||
|
||||
:recycle-delete-permanently
|
||||
(let [[root-id] args
|
||||
root-ref (cond
|
||||
(and (vector? root-id)
|
||||
(= :block/uuid (first root-id)))
|
||||
root-id
|
||||
|
||||
(uuid? root-id)
|
||||
[:block/uuid root-id]
|
||||
|
||||
:else
|
||||
root-id)
|
||||
root (d/entity @conn root-ref)
|
||||
tx-data (when root
|
||||
(seq (outliner-recycle/permanently-delete-tx-data @conn root)))]
|
||||
;; Keep replay idempotent under concurrent edits where the recycled root may
|
||||
;; already be permanently removed by a preceding remote tx.
|
||||
(when (seq tx-data)
|
||||
(ldb/transact! conn tx-data
|
||||
{:outliner-op :recycle-delete-permanently})))
|
||||
|
||||
:set-block-property
|
||||
(let [[block-eid property-id v] args
|
||||
block-eid' (or (replay-entity-id-value @conn block-eid)
|
||||
|
||||
@@ -1776,6 +1776,34 @@
|
||||
(is (= #{"page y"}
|
||||
(set (map :block/name (:user.property/x7 block'))))))))))
|
||||
|
||||
(deftest replay-recycle-delete-permanently-removes-recycled-page-test
|
||||
(testing "replay should permanently delete a recycled page subtree"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page 1"}
|
||||
:blocks [{:block/title "child 1"}]}])
|
||||
page (db-test/find-page-by-title @conn "page 1")
|
||||
child (db-test/find-block-by-content @conn "child 1")
|
||||
page-uuid (:block/uuid page)
|
||||
child-uuid (:block/uuid child)]
|
||||
(outliner-page/delete! conn page-uuid {})
|
||||
(is (true? (ldb/recycled? (d/entity @conn [:block/uuid page-uuid]))))
|
||||
(is (nil? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:recycle-delete-permanently [[:block/uuid page-uuid]]]
|
||||
nil)))
|
||||
(is (nil? (d/entity @conn [:block/uuid page-uuid])))
|
||||
(is (nil? (d/entity @conn [:block/uuid child-uuid]))))))
|
||||
|
||||
(deftest replay-recycle-delete-permanently-missing-root-is-idempotent-test
|
||||
(testing "replay should no-op when recycled root has already been removed"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:block/title "page 1"}}])
|
||||
missing-uuid (random-uuid)]
|
||||
(is (nil? (#'sync-apply/replay-canonical-outliner-op!
|
||||
conn
|
||||
[:recycle-delete-permanently [[:block/uuid missing-uuid]]]
|
||||
nil))))))
|
||||
|
||||
(deftest replay-set-block-property-converts-raw-uuid-to-eid-test
|
||||
(testing "replay should resolve raw block uuid ids for set-block-property"
|
||||
(let [graph {:classes {:tag1 {}}
|
||||
|
||||
Reference in New Issue
Block a user