feat: recycle

This commit is contained in:
Tienson Qin
2026-03-17 16:08:08 +08:00
parent dc829be3f1
commit 174cdfd865
37 changed files with 1153 additions and 410 deletions

View File

@@ -40,6 +40,7 @@
(defonce views-page-name "$$$views")
(defonce library-page-name "Library")
(defonce quick-add-page-name "Quick add")
(defonce recycle-page-name "Recycle")
(defn local-relative-asset?
[s]

View File

@@ -218,6 +218,7 @@
(def closed-value? entity-util/closed-value?)
(def journal? entity-util/journal?)
(def hidden? entity-util/hidden?)
(def recycled? entity-util/recycled?)
(def object? entity-util/object?)
(def asset? entity-util/asset?)
(def public-built-in-property? db-property/public-built-in-property?)

View File

@@ -343,9 +343,10 @@
user-datoms (get-all-user-datoms db)
pages-datoms (let [contents-id (get-first-page-by-title db "Contents")
capture-page-id (:db/id (db-db/get-built-in-page db common-config/quick-add-page-name))
views-id (get-first-page-by-title db common-config/views-page-name)]
views-id (get-first-page-by-title db common-config/views-page-name)
recycle-id (get-first-page-by-title db "Recycle")]
(mapcat #(d/datoms db :eavt %)
(remove nil? [contents-id capture-page-id views-id])))
(remove nil? [contents-id capture-page-id views-id recycle-id])))
data (->> (concat idents
structured-datoms
user-datoms

View File

@@ -295,7 +295,8 @@
exclude-ids (get-exclude-page-ids db)]
(keep (fn [d]
(let [e (entity-plus/unsafe->Entity db (:e d))]
(when-not (exclude-ids (:db/id e))
(when-not (or (exclude-ids (:db/id e))
(entity-util/hidden? e))
(cond-> e
refs-count?
(assoc :block.temp/refs-count (common-initial-data/get-block-refs-count db (:e d)))))))

View File

@@ -57,12 +57,34 @@
(defn hidden?
[page]
(boolean
(when page
(if (string? page)
(string/starts-with? page "$$$")
(when (or (map? page) (de/entity? page))
(:logseq.property/hide? page))))))
(letfn [(hidden-parent? [entity seen]
(when (and entity
(:db/id entity)
(not (contains? seen (:db/id entity))))
(or (:logseq.property/hide? entity)
(:logseq.property/deleted-at entity)
(hidden-parent? (:block/parent entity) (conj seen (:db/id entity))))))]
(boolean
(when page
(if (string? page)
(string/starts-with? page "$$$")
(when (or (map? page) (de/entity? page))
(or (:logseq.property/hide? page)
(:logseq.property/deleted-at page)
(hidden-parent? (:block/parent page) #{}))))))))
(defn recycled?
[entity]
(letfn [(recycled-parent? [parent seen]
(when (and parent
(:db/id parent)
(not (contains? seen (:db/id parent))))
(or (:logseq.property/deleted-at parent)
(recycled-parent? (:block/parent parent) (conj seen (:db/id parent))))))]
(boolean
(when (or (map? entity) (de/entity? entity))
(or (:logseq.property/deleted-at entity)
(recycled-parent? (:block/parent entity) #{}))))))
(defn object?
[node]

View File

@@ -561,7 +561,8 @@
:property
(entity-util/class? d)
:class
(entity-util/hidden? d)
(and (entity-util/page? d)
(true? (:logseq.property/hide? d)))
:hidden
;; TODO: Remove deprecated
(whiteboard? d)

View File

@@ -619,6 +619,26 @@
:schema {:type :entity
:hide? true}
:queryable? true}
:logseq.property/deleted-at {:title "Deleted at"
:schema {:type :datetime
:hide? true
:public? false}}
:logseq.property/deleted-by-ref {:title "Deleted by"
:schema {:type :entity
:hide? true
:public? false}}
:logseq.property.recycle/original-parent {:title "Recycle original parent"
:schema {:type :entity
:hide? true
:public? false}}
:logseq.property.recycle/original-page {:title "Recycle original page"
:schema {:type :entity
:hide? true
:public? false}}
:logseq.property.recycle/original-order {:title "Recycle original order"
:schema {:type :string
:hide? true
:public? false}}
:logseq.property.reaction/emoji-id {:title "Reaction emoji"
:schema {:type :string
:public? false

View File

@@ -30,7 +30,7 @@
(map (juxt :major :minor)
[(parse-schema-version x) (parse-schema-version y)])))
(def version (parse-schema-version "65.23"))
(def version (parse-schema-version "65.24"))
(defn major-version
"Return a number.

View File

@@ -182,6 +182,16 @@
:logseq.property/hide? true
:logseq.property/built-in? true})]))
(defn- build-recycle-page
[]
[(sqlite-util/block-with-timestamps
{:block/uuid (common-uuid/gen-uuid :builtin-block-uuid "Recycle")
:block/name (common-util/page-name-sanity-lc "Recycle")
:block/title "Recycle"
:block/tags [:logseq.class/Page]
:logseq.property/hide? true
:logseq.property/built-in? true})])
(defn- build-favorites-page
[]
[(sqlite-util/block-with-timestamps
@@ -247,7 +257,7 @@
default-classes (build-initial-classes db-ident->properties)
default-pages (->> (map sqlite-util/build-new-page built-in-pages-names)
(map mark-block-as-built-in))
hidden-pages (concat (build-initial-views) (build-favorites-page))
hidden-pages (concat (build-initial-views) (build-favorites-page) (build-recycle-page))
;; These classes bootstrap our tags and properties as they depend on each other e.g.
;; Root <-> Tag, classes-tx depends on logseq.property.class/extends, properties-tx depends on Property
bootstrap-class? (fn [c] (contains? #{:logseq.class/Root :logseq.class/Property :logseq.class/Tag :logseq.class/Template} (:db/ident c)))

View File

@@ -15,6 +15,7 @@
[logseq.db.sqlite.create-graph :as sqlite-create-graph]
[logseq.outliner.datascript :as ds]
[logseq.outliner.pipeline :as outliner-pipeline]
[logseq.outliner.recycle :as outliner-recycle]
[logseq.outliner.transaction :as outliner-tx]
[logseq.outliner.tree :as otree]
[logseq.outliner.validate :as outliner-validate]
@@ -796,11 +797,13 @@
(defn ^:api ^:large-vars/cleanup-todo delete-blocks
"Delete blocks from the tree."
[db blocks]
[db blocks opts]
(let [top-level-blocks (filter-top-level-blocks db blocks)
non-consecutive? (and (> (count top-level-blocks) 1) (seq (ldb/get-non-consecutive-blocks db top-level-blocks)))
top-level-blocks* (get-top-level-blocks top-level-blocks non-consecutive?)
top-level-blocks (remove :logseq.property/built-in? top-level-blocks*)
top-level-blocks (->> top-level-blocks*
(remove :logseq.property/built-in?)
(remove ldb/page?))
txs-state (ds/new-outliner-txs-state)
block-ids (map (fn [b] [:block/uuid (:block/uuid b)]) top-level-blocks)
start-block (first top-level-blocks)
@@ -827,9 +830,8 @@
(when (seq tx-data) (swap! txs-state concat tx-data)))
:else
(doseq [id block-ids]
(let [node (d/entity db id)]
(otree/-del node txs-state db))))))
(swap! txs-state concat
(outliner-recycle/recycle-blocks-tx-data db top-level-blocks opts)))))
{:tx-data @txs-state}))
(defn- move-to-original-position?
@@ -1067,8 +1069,8 @@
opts
(assoc opts :outliner-op :insert-blocks)))))
(let [f (fn [conn blocks _opts]
(delete-blocks @conn blocks))]
(let [f (fn [conn blocks opts]
(delete-blocks @conn blocks opts))]
(defn delete-blocks!
[conn blocks opts]
(op-transact! :delete-blocks f conn blocks opts)))

View File

@@ -120,7 +120,7 @@
[:delete-page
[:catn
[:op :keyword]
[:args [:tuple ::uuid]]]]
[:args [:tuple ::uuid ::option]]]]
[:toggle-reaction
[:catn
@@ -303,11 +303,9 @@
:transact-opts {:conn conn}
:local-tx? true)
*result (atom nil)]
(if (next ops)
(outliner-tx/transact!
opts'
(doseq [op-entry ops]
(apply-op! conn opts' *result op-entry)))
(apply-op! conn opts' *result (first ops)))
(outliner-tx/transact!
opts'
(doseq [op-entry ops]
(apply-op! conn opts' *result op-entry)))
@*result))

View File

@@ -18,6 +18,7 @@
[logseq.db.frontend.property.build :as db-property-build]
[logseq.graph-parser.block :as gp-block]
[logseq.graph-parser.text :as text]
[logseq.outliner.recycle :as outliner-recycle]
[logseq.outliner.validate :as outliner-validate]))
(defn- db-refs->page
@@ -46,49 +47,30 @@
(defn delete!
"Deletes a page. Returns true if able to delete page. If unable to delete,
calls error-handler fn and returns false"
[conn page-uuid & {:keys [persist-op? rename? error-handler]
[conn page-uuid & {:keys [persist-op? rename? error-handler deleted-by-uuid now-ms]
:or {persist-op? true
error-handler (fn [{:keys [msg]}] (js/console.error msg))}}]
(assert (uuid? page-uuid) (str ::delete! " wrong page-uuid: " (if page-uuid page-uuid "nil")))
(when page-uuid
(when-let [page (d/entity @conn [:block/uuid page-uuid])]
(let [blocks (:block/_page page)
truncate-blocks-tx-data (mapv
(fn [block]
[:db/retractEntity [:block/uuid (:block/uuid block)]])
blocks)]
;; TODO: maybe we should add $$$favorites to built-in pages?
(if (or (ldb/built-in? page) (ldb/hidden? page))
(do
(error-handler {:msg "Built-in page cannot be deleted"})
false)
(let [delete-property-tx (when (ldb/property? page)
(concat
(let [datoms (d/datoms @conn :avet (:db/ident page))]
(map (fn [d] [:db/retract (:e d) (:a d)]) datoms))
(map (fn [d] [:db/retractEntity (:e d)])
(d/datoms @conn :avet :logseq.property.history/property (:db/ident page)))))
today-page? (when-let [day (:block/journal-day page)]
(= (date-time-util/ms->journal-day (js/Date.)) day))
delete-page-tx (when-not today-page?
(concat (db-refs->page page)
delete-property-tx
[[:db/retractEntity (:db/id page)]]))
restore-class-parent-tx (->> (filter ldb/class? (:logseq.property.class/_extends page))
(map (fn [p]
{:db/id (:db/id p)
:logseq.property.class/extends :logseq.class/Root})))
tx-data (concat truncate-blocks-tx-data
restore-class-parent-tx
delete-page-tx)]
;; TODO: maybe we should add $$$favorites to built-in pages?
(if (or (ldb/built-in? page) (ldb/hidden? page))
(do
(error-handler {:msg "Built-in page cannot be deleted"})
false)
(let [today-page? (when-let [day (:block/journal-day page)]
(= (date-time-util/ms->journal-day (js/Date.)) day))
tx-data (when-not today-page?
(outliner-recycle/recycle-page-tx-data @conn page {:deleted-by-uuid deleted-by-uuid
:now-ms now-ms}))]
(when (seq tx-data)
(ldb/transact! conn tx-data
(cond-> {:outliner-op :delete-page
:deleted-page (str (:block/uuid page))
:deleted-page (:block/title page)
:persist-op? persist-op?}
rename?
(assoc :real-outliner-op :rename-page)))
true))))))
(assoc :real-outliner-op :rename-page))))
true)))))
(defn- build-page-tx [db properties page {:keys [class? tags class-ident-namespace]}]
(when (:block/uuid page)

View File

@@ -427,7 +427,7 @@
entities)
;; Delete property value block if it's no longer used by other blocks
retract-blocks-tx (when (seq deleting-entities)
(:tx-data (outliner-core/delete-blocks @conn deleting-entities)))]
(:tx-data (outliner-core/delete-blocks @conn deleting-entities {})))]
(concat
[[:db/retract (:db/id block) (:db/ident property)]]
retract-blocks-tx)))
@@ -847,7 +847,7 @@
{:type :notification
:payload {:message "The choice can't be deleted because it's built-in."
:type :warning}}))
(let [data (:tx-data (outliner-core/delete-blocks @conn [value-block]))
(let [data (:tx-data (outliner-core/delete-blocks @conn [value-block] {}))
tx-data (conj data (outliner-core/block-with-updated-at
{:db/id property-id}))]
(ldb/transact! conn tx-data)))))

View File

@@ -0,0 +1,256 @@
(ns logseq.outliner.recycle
"Recycle-based soft delete helpers for DB graphs"
(:require [datascript.core :as d]
[logseq.common.util :as common-util]
[logseq.common.uuid :as common-uuid]
[logseq.db :as ldb]
[logseq.db.common.initial-data :as common-initial-data]
[logseq.db.common.order :as db-order]))
(def ^:private recycle-page-title "Recycle")
(def retention-ms (* 60 24 3600 1000))
(def gc-interval-ms (* 24 3600 1000))
(defn recycled?
[entity]
(some? (:logseq.property/deleted-at entity)))
(defn- build-recycle-page-tx
[db-id]
(let [now (common-util/time-ms)]
{:db/id db-id
:block/uuid (common-uuid/gen-uuid :builtin-block-uuid recycle-page-title)
:block/name (common-util/page-name-sanity-lc recycle-page-title)
:block/title recycle-page-title
:block/tags [:logseq.class/Page]
:block/created-at now
:block/updated-at now
:logseq.property/hide? true
:logseq.property/built-in? true}))
(defn recycle-page
[db]
(ldb/get-built-in-page db recycle-page-title))
(defn- ensure-recycle-page
[db]
(if-let [page (recycle-page db)]
{:page page
:page-id (:db/id page)
:tx-data []}
{:page nil
:page-id "recycle-page"
:tx-data [(build-recycle-page-tx "recycle-page")]}))
(defn- next-child-order
[parent]
(let [last-child (last (ldb/sort-by-order (:block/_parent parent)))]
(db-order/gen-key (:block/order last-child) nil)))
(defn- maybe-assoc-ref
[m k entity]
(if (and entity (:db/id entity))
(assoc m k (:db/id entity))
m))
(defn- maybe-assoc
[m k v]
(if (some? v)
(assoc m k v)
m))
(defn- resolve-entity
[db value]
(cond
(and value (:db/id value)) value
(int? value) (d/entity db value)
(vector? value) (d/entity db value)
:else nil))
(defn- block-subtree
[db block]
(let [ids (cons (:db/id block)
(common-initial-data/get-block-full-children-ids db (:db/id block)))]
(keep #(d/entity db %) ids)))
(defn- page-descendants
[page]
(loop [pages [page]
result []]
(if-let [page' (first pages)]
(let [children (->> (:block/_parent page')
(filter ldb/page?)
ldb/sort-by-order)]
(recur (concat (rest pages) children)
(conj result page')))
result)))
(defn- page-block-subtree-ids
[db page]
(->> (:block/_page page)
ldb/sort-by-order
(mapcat (fn [block]
(map :db/id (block-subtree db block))))))
(defn- page-tree-ids
[db page]
(->> (page-descendants page)
(mapcat (fn [page']
(cons (:db/id page')
(page-block-subtree-ids db page'))))
distinct))
(defn- deleted-by-id
[db deleted-by-uuid]
(some-> deleted-by-uuid
(#(d/entity db [:block/uuid %]))
:db/id))
(defn recycle-blocks-tx-data
[db blocks {:keys [deleted-by-uuid now-ms]}]
(let [{:keys [page page-id tx-data]} (ensure-recycle-page db)
deleted-by-id (deleted-by-id db deleted-by-uuid)
now-ms (or now-ms (common-util/time-ms))]
(let [[recycle-tx _previous-order]
(reduce
(fn [[txs previous-order] block]
(let [subtree (block-subtree db block)
order (db-order/gen-key previous-order nil)
root-tx (cond-> {:db/id (:db/id block)
:block/parent page-id
:block/page page-id
:block/order order
:logseq.property/deleted-at now-ms}
true
(maybe-assoc-ref :logseq.property/deleted-by-ref (d/entity db deleted-by-id))
true
(maybe-assoc-ref :logseq.property.recycle/original-parent (:block/parent block))
true
(maybe-assoc-ref :logseq.property.recycle/original-page (:block/page block))
true
(maybe-assoc :logseq.property.recycle/original-order (:block/order block)))
subtree-page-tx (map (fn [node]
{:db/id (:db/id node)
:block/page page-id})
subtree)]
[(into txs (cons root-tx (rest subtree-page-tx))) order]))
[[] (some->> page :block/_parent ldb/sort-by-order last :block/order)]
blocks)]
(concat tx-data recycle-tx))))
(defn recycle-page-tx-data
[db page {:keys [deleted-by-uuid now-ms]}]
(let [{recycle-page-id :page-id
recycle-page-tx-data :tx-data
recycle-page-existing :page} (ensure-recycle-page db)
deleted-by-id (deleted-by-id db deleted-by-uuid)
now-ms (or now-ms (common-util/time-ms))]
(concat recycle-page-tx-data
[(cond-> {:db/id (:db/id page)
:block/parent recycle-page-id
:block/order (if recycle-page-existing
(next-child-order recycle-page-existing)
(db-order/gen-key nil nil))
:logseq.property/deleted-at now-ms}
true
(maybe-assoc-ref :logseq.property/deleted-by-ref (d/entity db deleted-by-id))
true
(maybe-assoc-ref :logseq.property.recycle/original-parent (:block/parent page))
true
(maybe-assoc-ref :logseq.property.recycle/original-page page)
true
(maybe-assoc :logseq.property.recycle/original-order (:block/order page)))])))
(defn- restore-order
[target-parent]
(next-child-order target-parent))
(defn- restore-target
[db root]
(let [original-parent (resolve-entity db (:logseq.property.recycle/original-parent root))
original-page (resolve-entity db (:logseq.property.recycle/original-page root))
parent-valid? (and original-parent
(not (recycled? original-parent))
(d/entity db (:db/id original-parent)))]
(cond
(ldb/page? root)
{:parent (when parent-valid? original-parent)
:page root
:order (or (:logseq.property.recycle/original-order root)
(when parent-valid? (restore-order original-parent)))}
parent-valid?
{:parent original-parent
:page original-page
:order (or (:logseq.property.recycle/original-order root)
(restore-order original-parent))}
(and original-page
(d/entity db (:db/id original-page))
(not (recycled? original-page)))
{:parent original-page
:page original-page
:order (restore-order original-page)}
:else
nil)))
(defn restore-tx-data
[db root]
(when-let [{:keys [parent page order]} (restore-target db root)]
(let [subtree (when-not (ldb/page? root)
(block-subtree db root))
clear-structure [[:db/retract (:db/id root) :block/parent]
[:db/retract (:db/id root) :block/order]
(when-not (ldb/page? root)
[:db/retract (:db/id root) :block/page])]
clear-meta [[:db/retract (:db/id root) :logseq.property/deleted-at]
[:db/retract (:db/id root) :logseq.property/deleted-by-ref]
[:db/retract (:db/id root) :logseq.property.recycle/original-parent]
[:db/retract (:db/id root) :logseq.property.recycle/original-page]
[:db/retract (:db/id root) :logseq.property.recycle/original-order]]
root-tx (cond-> {:db/id (:db/id root)}
parent
(assoc :block/parent (:db/id parent))
order
(assoc :block/order order)
(not (ldb/page? root))
(assoc :block/page (:db/id page)))
subtree-page-tx (when (seq subtree)
(map (fn [node]
{:db/id (:db/id node)
:block/page (:db/id page)})
subtree))]
(concat clear-structure [root-tx] subtree-page-tx (remove nil? clear-meta)))))
(defn restore!
[conn root-uuid]
(when-let [root (d/entity @conn [:block/uuid root-uuid])]
(when-let [tx-data (seq (restore-tx-data @conn root))]
(ldb/transact! conn tx-data {:outliner-op :restore-recycled})
true)))
(defn gc-tx-data
[db {:keys [now-ms] :or {now-ms (common-util/time-ms)}}]
(let [cutoff (- now-ms retention-ms)]
(->>
(d/q '[:find [?e ...]
:in $ ?cutoff
:where
[?e :logseq.property/deleted-at ?deleted-at]
[(<= ?deleted-at ?cutoff)]]
db cutoff)
(map #(d/entity db %))
(filter recycled?)
(mapcat (fn [entity]
(if (ldb/page? entity)
(map (fn [id] [:db/retractEntity id]) (page-tree-ids db entity))
(map (fn [node] [:db/retractEntity (:db/id node)]) (block-subtree db entity)))))
distinct)))
(defn gc!
[conn opts]
(when-let [tx-data (seq (gc-tx-data @conn opts))]
(ldb/transact! conn tx-data {:outliner-op :recycle-gc
:persist-op? false})
true))

View File

@@ -6,16 +6,20 @@
[logseq.outliner.core :as outliner-core]))
(deftest test-delete-block-with-default-property
(testing "Delete block with default property"
(testing "Delete block with default property moves the block to recycle"
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "b1" :build/properties {:default "test block"}}]}])
property-value (:user.property/default (db-test/find-block-by-content @conn "b1"))
_ (assert (:db/id property-value))
block (db-test/find-block-by-content @conn "b1")]
(outliner-core/delete-blocks! conn [block] {})
(is (nil? (db-test/find-block-by-content @conn "b1")))
(is (nil? (db-test/find-block-by-content @conn "test block"))))))
(let [block' (db-test/find-block-by-content @conn "b1")
property-value (:user.property/default block')
recycle-page (ldb/get-built-in-page @conn "Recycle")]
(is (some? block'))
(is (some? property-value))
(is (integer? (:logseq.property/deleted-at block')))
(is (= (:db/id recycle-page) (:db/id (:block/page block'))))
(is (= (:db/id recycle-page) (:db/id (:block/page property-value))))))))
(deftest test-delete-page-with-outliner-core
(testing "Pages shouldn't be deleted through outliner-core/delete-blocks"
@@ -37,5 +41,36 @@
(is (some? (db-test/find-block-by-content @conn "b4")))
(let [page2' (ldb/get-page @conn "page2")]
(is (= "page2" (:block/title page2')))
(is (nil? (:block/parent page2')))
(is (nil? (:block/order page2')))))))
(is (= (:db/id page1) (:db/id (:block/parent page2'))))
(is (= "a1" (:block/order page2')))))))
(deftest delete-blocks-moves-subtree-to-recycle
(let [user-uuid (random-uuid)
conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}]}])
recycle-page (ldb/get-built-in-page @conn "Recycle")
page (ldb/get-page @conn "page1")
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")
original-order (:block/order parent)]
(d/transact! conn [{:block/uuid user-uuid
:block/title "Alice"}])
(outliner-core/delete-blocks! conn [parent] {:deleted-by-uuid user-uuid})
(let [parent' (db-test/find-block-by-content @conn "parent")
child' (db-test/find-block-by-content @conn "child")
recycle-page (ldb/get-built-in-page @conn "Recycle")]
(is (some? parent'))
(is (some? child'))
(is (= (:block/uuid recycle-page) (:block/uuid (:block/parent parent'))))
(is (= (:block/uuid recycle-page) (:block/uuid (:block/page parent'))))
(is (integer? (:logseq.property/deleted-at parent')))
(is (= user-uuid
(:block/uuid (d/entity @conn (:logseq.property/deleted-by-ref parent')))))
(is (= (:block/uuid page)
(:block/uuid (d/entity @conn (:logseq.property.recycle/original-page parent')))))
(is (= original-order (:logseq.property.recycle/original-order parent')))
(is (= (:block/uuid parent') (:block/uuid (:block/parent child'))))
(is (= (:block/uuid recycle-page) (:block/uuid (:block/page child'))))
(is (nil? (:logseq.property/deleted-at child'))))))

View File

@@ -108,8 +108,14 @@
(is (contains? (set (map :db/id (:block/refs (d/entity @conn (:db/id b1)))))
(:db/id d1)))
(outliner-page/delete! conn (:block/uuid d1))
(is (nil? (d/entity @conn (:db/id d1))))
(is (nil? (d/entity @conn (:db/id b1))))))
(let [d1' (d/entity @conn (:db/id d1))
b1' (d/entity @conn (:db/id b1))
recycle-page (ldb/get-built-in-page @conn "Recycle")]
(is (some? d1'))
(is (some? b1'))
(is (= (:block/uuid recycle-page) (:block/uuid (:block/parent d1'))))
(is (integer? (:logseq.property/deleted-at d1')))
(is (= (:block/uuid d1') (:block/uuid (:block/page b1')))))))
(deftest create-journal
(let [conn (db-test/create-conn)

View File

@@ -305,7 +305,11 @@
_ (assert (:user.property/default (db-test/find-block-by-content @conn "b1")))
property-id (:db/id (d/entity @conn :user.property/default))
_ (outliner-property/delete-closed-value! conn property-id [:block/uuid closed-value-uuid])]
(is (nil? (d/entity @conn [:block/uuid closed-value-uuid])))))
(let [closed-value (d/entity @conn [:block/uuid closed-value-uuid])
recycle-page (ldb/get-built-in-page @conn "Recycle")]
(is (some? closed-value))
(is (integer? (:logseq.property/deleted-at closed-value)))
(is (= (:db/id recycle-page) (:db/id (:block/page closed-value)))))))
(deftest class-add-property!
(let [conn (db-test/create-conn-with-blocks

View File

@@ -0,0 +1,75 @@
(ns logseq.outliner.recycle-test
(:require [cljs.test :refer [deftest is testing]]
[datascript.core :as d]
[logseq.db :as ldb]
[logseq.db.test.helper :as db-test]
[logseq.outliner.core :as outliner-core]
[logseq.outliner.recycle :as recycle]))
(deftest restore-recycled-block-returns-subtree-to-original-location
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}
{:block/title "sibling"}]}])
page (ldb/get-page @conn "page1")
parent (db-test/find-block-by-content @conn "parent")]
(outliner-core/delete-blocks! conn [parent] {})
(recycle/restore! conn (:block/uuid parent))
(let [parent' (db-test/find-block-by-content @conn "parent")
child' (db-test/find-block-by-content @conn "child")]
(is (= (:block/uuid page) (:block/uuid (:block/parent parent'))))
(is (= (:block/uuid page) (:block/uuid (:block/page parent'))))
(is (= (:block/uuid parent') (:block/uuid (:block/parent child'))))
(is (= (:block/uuid page) (:block/uuid (:block/page child'))))
(is (nil? (:logseq.property/deleted-at parent')))
(is (nil? (:logseq.property/deleted-by-ref parent')))
(is (nil? (:logseq.property.recycle/original-parent parent')))
(is (nil? (:logseq.property.recycle/original-page parent')))
(is (nil? (:logseq.property.recycle/original-order parent'))))))
(deftest restore-recycled-block-falls-back-to-page-root-when-original-parent-is-unavailable
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}
{:block/title "sibling"}]}])
page (ldb/get-page @conn "page1")
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")]
(outliner-core/delete-blocks! conn [child] {})
(outliner-core/delete-blocks! conn [parent] {})
(recycle/restore! conn (:block/uuid child))
(let [child' (db-test/find-block-by-content @conn "child")]
(is (= (:block/uuid page) (:block/uuid (:block/parent child'))))
(is (= (:block/uuid page) (:block/uuid (:block/page child'))))
(is (nil? (:logseq.property/deleted-at child'))))))
(deftest restore-recycled-page-removes-recycle-parent
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "b1"}]}])
page (ldb/get-page @conn "page1")]
(recycle/recycle-page-tx-data @conn page {})
(ldb/transact! conn (recycle/recycle-page-tx-data @conn page {}) {:outliner-op :delete-page})
(recycle/restore! conn (:block/uuid page))
(let [page' (ldb/get-page @conn "page1")]
(is (nil? (:block/parent page')))
(is (nil? (:logseq.property/deleted-at page')))
(is (nil? (:logseq.property.recycle/original-parent page'))))))
(deftest gc-retracts-recycled-subtrees-older-than-retention-window
(let [now-ms 1000
old-ms (- now-ms (* 61 24 3600 1000))
conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}
:blocks [{:block/title "parent"
:build/children [{:block/title "child"}]}]}])
parent (db-test/find-block-by-content @conn "parent")
child (db-test/find-block-by-content @conn "child")]
(outliner-core/delete-blocks! conn [parent] {})
(d/transact! conn [{:db/id (:db/id (db-test/find-block-by-content @conn "parent"))
:logseq.property/deleted-at old-ms}])
(recycle/gc! conn {:now-ms now-ms})
(is (nil? (d/entity @conn [:block/uuid (:block/uuid parent)])))
(is (nil? (d/entity @conn [:block/uuid (:block/uuid child)])))))