mirror of
https://github.com/logseq/logseq.git
synced 2026-06-01 19:01:22 +00:00
feat: recycle
This commit is contained in:
1
deps/common/src/logseq/common/config.cljs
vendored
1
deps/common/src/logseq/common/config.cljs
vendored
@@ -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]
|
||||
|
||||
1
deps/db/src/logseq/db.cljs
vendored
1
deps/db/src/logseq/db.cljs
vendored
@@ -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?)
|
||||
|
||||
@@ -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
|
||||
|
||||
3
deps/db/src/logseq/db/common/view.cljs
vendored
3
deps/db/src/logseq/db/common/view.cljs
vendored
@@ -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)))))))
|
||||
|
||||
34
deps/db/src/logseq/db/frontend/entity_util.cljs
vendored
34
deps/db/src/logseq/db/frontend/entity_util.cljs
vendored
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
deps/db/src/logseq/db/frontend/property.cljs
vendored
20
deps/db/src/logseq/db/frontend/property.cljs
vendored
@@ -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
|
||||
|
||||
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
2
deps/db/src/logseq/db/frontend/schema.cljs
vendored
@@ -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.
|
||||
|
||||
12
deps/db/src/logseq/db/sqlite/create_graph.cljs
vendored
12
deps/db/src/logseq/db/sqlite/create_graph.cljs
vendored
@@ -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)))
|
||||
|
||||
16
deps/outliner/src/logseq/outliner/core.cljs
vendored
16
deps/outliner/src/logseq/outliner/core.cljs
vendored
@@ -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)))
|
||||
|
||||
12
deps/outliner/src/logseq/outliner/op.cljs
vendored
12
deps/outliner/src/logseq/outliner/op.cljs
vendored
@@ -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))
|
||||
|
||||
50
deps/outliner/src/logseq/outliner/page.cljs
vendored
50
deps/outliner/src/logseq/outliner/page.cljs
vendored
@@ -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)
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
256
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
Normal file
256
deps/outliner/src/logseq/outliner/recycle.cljs
vendored
Normal 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))
|
||||
@@ -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'))))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
75
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal file
75
deps/outliner/test/logseq/outliner/recycle_test.cljs
vendored
Normal 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)])))))
|
||||
Reference in New Issue
Block a user