mirror of https://github.com/logseq/logseq
feat(outliner): sync to file
parent
2f790d3a34
commit
3b8a825b24
|
@ -281,6 +281,12 @@
|
|||
@conn)
|
||||
(flatten))))
|
||||
|
||||
(defn get-file-by-path
|
||||
[file-path]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(when-let [conn (conn/get-files-conn repo)]
|
||||
(d/pull @conn '[*] [:file/path file-path]))))
|
||||
|
||||
(defn get-custom-css
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
|
@ -638,6 +644,18 @@
|
|||
block-uuid)
|
||||
sort-by-left)))
|
||||
|
||||
(defn get-blocks-by-page
|
||||
[id-or-lookup-ref]
|
||||
(when-let [conn (conn/get-conn)]
|
||||
(->
|
||||
(d/q
|
||||
'[:find (pull ?block [*])
|
||||
:in $ ?page
|
||||
:where
|
||||
[?block :block/page ?page]]
|
||||
conn id-or-lookup-ref)
|
||||
flatten)))
|
||||
|
||||
(defn get-block-children
|
||||
"Including nested children."
|
||||
[repo block-uuid]
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
(:require [datascript.core :as d]
|
||||
[frontend.db.react :as react]
|
||||
[frontend.util :as util]
|
||||
[frontend.debug :as debug]))
|
||||
[frontend.debug :as debug]
|
||||
[frontend.db.utils :as db-utils]))
|
||||
|
||||
(defn get-by-id
|
||||
[conn id]
|
||||
|
@ -29,8 +30,10 @@
|
|||
(defn save-block
|
||||
[conn block-m]
|
||||
(let [tx (-> (dissoc block-m :block/children)
|
||||
(util/remove-nils))]
|
||||
(d/transact! conn [tx])))
|
||||
(util/remove-nils))
|
||||
lookup-ref (:block/uuid block-m)]
|
||||
(d/transact! conn [tx])
|
||||
(db-utils/pull [:block/uuid lookup-ref])))
|
||||
|
||||
(defn del-block
|
||||
[conn id-or-look-ref]
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
[cljs-time.coerce :as tc]
|
||||
[cljs-time.core :as t]
|
||||
[frontend.modules.outliner.core :as outliner-core]
|
||||
[frontend.modules.outliner.tree :as tree]))
|
||||
[frontend.modules.outliner.tree :as tree]
|
||||
[lambdaisland.glogi :as log]
|
||||
[frontend.debug :as debug]))
|
||||
|
||||
(defn- remove-block-child!
|
||||
[target-block parent-block]
|
||||
|
@ -367,22 +369,25 @@
|
|||
2. Sometimes we might need to move a parent block to it's own child.
|
||||
"
|
||||
[current-block target-block top? nested?]
|
||||
(let [[current-node target-node]
|
||||
(mapv outliner-core/block [current-block target-block])]
|
||||
(cond
|
||||
top?
|
||||
(let [first-child?
|
||||
(= (tree/-get-parent-id target-node)
|
||||
(tree/-get-left-id target-node))]
|
||||
(if first-child?
|
||||
(let [parent (tree/-get-parent target-node)]
|
||||
(outliner-core/move-subtree current-node parent false))
|
||||
(outliner-core/move-subtree current-node target-node true)))
|
||||
nested?
|
||||
(outliner-core/move-subtree current-node target-node false)
|
||||
(if-not (every? map? [current-block target-block])
|
||||
(log/error :dnd/move-block-argument-error
|
||||
{:current-block current-block :target-block target-block})
|
||||
(let [[current-node target-node]
|
||||
(mapv outliner-core/block [current-block target-block])]
|
||||
(cond
|
||||
top?
|
||||
(let [first-child?
|
||||
(= (tree/-get-parent-id target-node)
|
||||
(tree/-get-left-id target-node))]
|
||||
(if first-child?
|
||||
(let [parent (tree/-get-parent target-node)]
|
||||
(outliner-core/move-subtree current-node parent false))
|
||||
(outliner-core/move-subtree current-node target-node true)))
|
||||
nested?
|
||||
(outliner-core/move-subtree current-node target-node false)
|
||||
|
||||
:else
|
||||
(outliner-core/move-subtree current-node target-node true))
|
||||
(let [repo (state/get-current-repo)]
|
||||
(db/refresh repo {:key :block/change
|
||||
:data [(:data current-node) (:data target-node)]}))))
|
||||
:else
|
||||
(outliner-core/move-subtree current-node target-node true))
|
||||
(let [repo (state/get-current-repo)]
|
||||
(db/refresh repo {:key :block/change
|
||||
:data [(:data current-node) (:data target-node)]})))))
|
||||
|
|
|
@ -561,7 +561,7 @@
|
|||
(->
|
||||
(wrap-parse-block block)
|
||||
(outliner-core/block)
|
||||
(tree/-save))
|
||||
(outliner-core/save-node))
|
||||
(let [opts {:key :block/change
|
||||
:data [block]}]
|
||||
(db/refresh repo opts))))
|
||||
|
@ -768,7 +768,7 @@
|
|||
(mapv outliner-core/block [current-block new-block])
|
||||
has-children? (seq (tree/-get-children current-node))
|
||||
new-is-sibling? (not has-children?)]
|
||||
(tree/-save current-node)
|
||||
(outliner-core/save-node current-node)
|
||||
(outliner-core/insert-node new-node current-node new-is-sibling?)))
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
(ns frontend.modules.file.core
|
||||
(:require [frontend.debug :as debug]
|
||||
[clojure.string :as str]
|
||||
[frontend.state :as state]
|
||||
[cljs.core.async :as async]
|
||||
[frontend.db.conn :as conn]
|
||||
[frontend.db.utils :as db-utils]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.modules.outliner.tree :as tree]))
|
||||
|
||||
(defn clip-content
|
||||
[content]
|
||||
(->
|
||||
(str/replace content #"^\n+" "")
|
||||
(str/replace #"^#+" "")
|
||||
(str/replace #"\n+$" "")))
|
||||
|
||||
(defn transform-content
|
||||
[pre-block? content level]
|
||||
(if pre-block?
|
||||
(clip-content content)
|
||||
(let [prefix (->>
|
||||
(repeat level "#")
|
||||
(apply str))
|
||||
new-content (clip-content content)]
|
||||
(str prefix " " new-content))))
|
||||
|
||||
(defn tree->file-content
|
||||
[tree init-level]
|
||||
(loop [block-contents []
|
||||
[f & r] tree
|
||||
level init-level]
|
||||
(if (nil? f)
|
||||
(str/join "\n" block-contents)
|
||||
(let [content (transform-content
|
||||
(:block/pre-block? f) (:block/content f) level)
|
||||
new-content
|
||||
(if-let [children (seq (:block/children f))]
|
||||
[content (tree->file-content children (inc level))]
|
||||
[content])]
|
||||
(recur (into block-contents new-content) r level)))))
|
||||
|
||||
(def markdown-init-level 2)
|
||||
|
||||
(defn push-to-write-chan
|
||||
[files file->content & opts]
|
||||
(let [repo (state/get-current-repo)]
|
||||
(when-let [chan (state/get-file-write-chan)]
|
||||
(let [chan-callback (:chan-callback opts)]
|
||||
(async/put! chan [repo files opts file->content])
|
||||
(when chan-callback
|
||||
(chan-callback))))))
|
||||
|
||||
(defn save-tree
|
||||
[page-block tree]
|
||||
{:pre [(map? page-block)]}
|
||||
(let [new-content (tree->file-content tree markdown-init-level)
|
||||
file-db-id (-> page-block :block/file :db/id)
|
||||
file-path (-> (db-utils/entity file-db-id) :file/path)
|
||||
_ (assert (string? file-path) "File path should satisfy string?")
|
||||
{old-content :file/content :as file} (model/get-file-by-path file-path)
|
||||
files [[file-path new-content]]
|
||||
file->content {file-path old-content}]
|
||||
(push-to-write-chan files file->content)))
|
|
@ -7,12 +7,15 @@
|
|||
[frontend.modules.outliner.ref :as outliner-ref]
|
||||
[frontend.state :as state]
|
||||
[frontend.debug :as debug]
|
||||
[clojure.set :as set]))
|
||||
[clojure.set :as set]
|
||||
[frontend.modules.outliner.file :as outliner-file]
|
||||
[frontend.util :as util]))
|
||||
|
||||
(defrecord Block [data])
|
||||
|
||||
(defn block
|
||||
[m]
|
||||
(assert (map? m) (util/format "block data must be map,got: %s %s" (type m) m))
|
||||
(->Block m))
|
||||
|
||||
(defn get-block-by-id
|
||||
|
@ -97,9 +100,9 @@
|
|||
|
||||
(-save [this]
|
||||
(let [conn (conn/get-conn false)
|
||||
data (:data this)]
|
||||
(db-outliner/save-block conn data)
|
||||
this))
|
||||
data (:data this)
|
||||
new-data (db-outliner/save-block conn data)]
|
||||
(assoc this :data new-data)))
|
||||
|
||||
(-del [this]
|
||||
(let [conn (conn/get-conn false)
|
||||
|
@ -126,44 +129,51 @@
|
|||
(throw (js/Error. "Number of children and sorted-children are not equal."))))
|
||||
sorted-children)))))))))
|
||||
|
||||
|
||||
(defn save-node
|
||||
[node]
|
||||
{:pre [(tree/satisfied-inode? node)]}
|
||||
(let [saved-node (tree/-save node)]
|
||||
(outliner-file/sync-to-file saved-node)))
|
||||
|
||||
(defn insert-node-as-first-child
|
||||
"Insert a node as first child."
|
||||
[new-node parent-node]
|
||||
(:pre [(every? tree/satisfied-inode? [new-node parent-node])])
|
||||
{:pre [(every? tree/satisfied-inode? [new-node parent-node])]}
|
||||
(let [parent-id (tree/-get-id parent-node)
|
||||
node (-> (tree/-set-left-id new-node parent-id)
|
||||
(tree/-set-parent-id parent-id))
|
||||
right-node (tree/-get-down parent-node)]
|
||||
(do
|
||||
(if (tree/satisfied-inode? right-node)
|
||||
(let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))]
|
||||
(tree/-save node)
|
||||
(tree/-save new-right-node))
|
||||
(tree/-save node))
|
||||
(let [repo (state/get-current-repo)]
|
||||
(outliner-state/update-block-state repo node)))))
|
||||
(let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))
|
||||
saved-new-node (tree/-save node)]
|
||||
(tree/-save new-right-node)
|
||||
saved-new-node)
|
||||
(tree/-save node)))))
|
||||
|
||||
(defn insert-node-as-sibling
|
||||
"Insert a node as sibling."
|
||||
[new-node left-node]
|
||||
(:pre [(every? tree/satisfied-inode? [new-node left-node])])
|
||||
{:pre [(every? tree/satisfied-inode? [new-node left-node])]}
|
||||
(let [node (-> (tree/-set-left-id new-node (tree/-get-id left-node))
|
||||
(tree/-set-parent-id (tree/-get-parent-id left-node)))
|
||||
right-node (tree/-get-right left-node)]
|
||||
(do
|
||||
(if (tree/satisfied-inode? right-node)
|
||||
(let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))]
|
||||
(tree/-save node)
|
||||
(tree/-save new-right-node))
|
||||
(tree/-save node))
|
||||
(let [repo (state/get-current-repo)]
|
||||
(outliner-state/update-block-state repo node)))))
|
||||
(let [new-right-node (tree/-set-left-id right-node (tree/-get-id new-node))
|
||||
saved-new-node (tree/-save node)]
|
||||
(tree/-save new-right-node)
|
||||
saved-new-node)
|
||||
(tree/-save node)))))
|
||||
|
||||
(defn insert-node
|
||||
[new-node target-node sibling?]
|
||||
(if sibling?
|
||||
(insert-node-as-sibling new-node target-node)
|
||||
(insert-node-as-first-child new-node target-node)))
|
||||
(let [saved-node
|
||||
(if sibling?
|
||||
(insert-node-as-sibling new-node target-node)
|
||||
(insert-node-as-first-child new-node target-node))]
|
||||
(outliner-file/sync-to-file saved-node)))
|
||||
|
||||
(defn delete-node
|
||||
"Delete node from the tree."
|
||||
|
@ -175,8 +185,7 @@
|
|||
(let [left-node (tree/-get-left node)
|
||||
new-right-node (tree/-set-left-id right-node (tree/-get-id left-node))]
|
||||
(tree/-save new-right-node)))
|
||||
(let [repo (state/get-current-repo)]
|
||||
(outliner-state/update-block-state repo node))))
|
||||
(outliner-file/sync-to-file node)))
|
||||
|
||||
(defn move-subtree
|
||||
"Move subtree to a destination position in the relation tree.
|
||||
|
@ -194,4 +203,10 @@
|
|||
(tree/-save new-right-node)))
|
||||
(if sibling
|
||||
(insert-node-as-sibling root target-node)
|
||||
(insert-node-as-first-child root target-node))))
|
||||
(insert-node-as-first-child root target-node))
|
||||
(do
|
||||
|
||||
(outliner-file/sync-to-file root)
|
||||
;; TODO Should sync to file where the target-node located when the
|
||||
;; target-node is in another file.
|
||||
)))
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
(ns frontend.modules.outliner.file
|
||||
(:require [frontend.state :as state]
|
||||
[frontend.db.model :as model]
|
||||
[frontend.modules.outliner.tree :as tree]
|
||||
[frontend.modules.file.core :as file]
|
||||
[frontend.debug :as debug]
|
||||
[frontend.db.utils :as db-utils]))
|
||||
|
||||
(defn sync-to-file
|
||||
[updated-block]
|
||||
{:pre [(tree/satisfied-inode? updated-block)]}
|
||||
(let [page-db-id (-> updated-block :data :block/page :db/id)
|
||||
blocks (model/get-blocks-by-page page-db-id)
|
||||
tree (tree/blocks->vec-tree blocks)
|
||||
page-block (db-utils/pull page-db-id)]
|
||||
(file/save-tree page-block tree)))
|
|
@ -1,7 +1,8 @@
|
|||
(ns frontend.modules.outliner.tree
|
||||
(:require [frontend.modules.outliner.ref :as outliner-ref]
|
||||
[clojure.set :as set]
|
||||
[frontend.modules.outliner.utils :as outliner-u]))
|
||||
[frontend.modules.outliner.utils :as outliner-u]
|
||||
[frontend.debug :as debug]))
|
||||
|
||||
(defprotocol INode
|
||||
(-get-id [this])
|
||||
|
@ -54,16 +55,16 @@
|
|||
(recur db-id (conj children child))
|
||||
children)))
|
||||
|
||||
(comment
|
||||
(defn- clip-block
|
||||
"For debug. It's should be removed."
|
||||
[x]
|
||||
(let [ks [:block/parent :block/left :block/pre-block? :block/uuid
|
||||
:block/level :block/title :db/id]]
|
||||
(map #(select-keys % ks) x))))
|
||||
(defn- clip-block
|
||||
"For debug. It's should be removed."
|
||||
[x]
|
||||
(let [ks [:block/parent :block/left :block/pre-block? :block/uuid
|
||||
:block/level :block/title :db/id :block/page]]
|
||||
(map #(select-keys % ks) x)))
|
||||
|
||||
(defn blocks->vec-tree
|
||||
[blocks]
|
||||
;; (debug/pprint "blocks->vec-tree" blocks)
|
||||
(let [{:keys [ids parents indexed-by-position]}
|
||||
(prepare-blocks blocks)
|
||||
root-id (first (set/difference parents ids))]
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
(ns frontend.modules.file.core-test
|
||||
(:require [cljs.test :refer [deftest is are testing use-fixtures run-tests] :as test]
|
||||
[cljs-run-test :refer [run-test]]
|
||||
[frontend.modules.file.core :as file]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(deftest test-transform-content
|
||||
(let [s "#### abc\n\n"
|
||||
r "abc"]
|
||||
(is (= r (file/clip-content s)))))
|
||||
|
||||
(def tree
|
||||
'({:block/pre-block? true,
|
||||
:block/uuid #uuid "60643869-3b5b-4e1b-a1f8-28ec965abb4c",
|
||||
:block/left {:db/id 20},
|
||||
:block/body
|
||||
({:block/uuid #uuid "60643869-6feb-4b4a-87b0-7a8a76cf2746",
|
||||
:block/refs (),
|
||||
:block/anchor "level_1",
|
||||
:block/children
|
||||
#{[:block/uuid #uuid "60643869-c9dc-4a06-b1c3-e86bad631b3a"]},
|
||||
:block/body [],
|
||||
:block/meta
|
||||
{:timestamps [], :properties [], :start-pos 30, :end-pos 41},
|
||||
:block/level 2,
|
||||
:block/tags [],
|
||||
:block/title [["Plain" "level 1"]]}
|
||||
{:block/uuid #uuid "60643869-c9dc-4a06-b1c3-e86bad631b3a",
|
||||
:block/refs (),
|
||||
:block/anchor "level_1_1",
|
||||
:block/children
|
||||
#{[:block/uuid #uuid "60643869-3d80-4926-8854-5de911fb2aca"]},
|
||||
:block/body [],
|
||||
:block/meta
|
||||
{:timestamps [], :properties [], :start-pos 41, :end-pos 55},
|
||||
:block/level 3,
|
||||
:block/tags [],
|
||||
:block/title [["Plain" "level 1-1"]]}
|
||||
{:block/uuid #uuid "60643869-3d80-4926-8854-5de911fb2aca",
|
||||
:block/refs (),
|
||||
:block/anchor "level_1_1_1",
|
||||
:block/children #{},
|
||||
:block/body [],
|
||||
:block/meta
|
||||
{:timestamps [], :properties [], :start-pos 55, :end-pos 72},
|
||||
:block/level 4,
|
||||
:block/tags [],
|
||||
:block/title [["Plain" "level 1-1-1"]]}
|
||||
{:block/uuid #uuid "60643869-39d5-497e-b300-fa49993f6fda",
|
||||
:block/refs (),
|
||||
:block/anchor "level_2",
|
||||
:block/children #{},
|
||||
:block/body [],
|
||||
:block/meta
|
||||
{:timestamps [], :properties [], :start-pos 72, :end-pos 83},
|
||||
:block/level 2,
|
||||
:block/tags [],
|
||||
:block/title [["Plain" "level 2"]]}),
|
||||
:block/format :markdown,
|
||||
:block/level 2,
|
||||
:block/refs-with-children (),
|
||||
:block/content "---\ntitle: Mar 31th, 2021\n---\n",
|
||||
:db/id 24,
|
||||
:block/parent {:db/id 20},
|
||||
:block/page {:db/id 20},
|
||||
:block/file {:db/id 16}}
|
||||
{:block/uuid #uuid "60643869-6feb-4b4a-87b0-7a8a76cf2746",
|
||||
:block/left {:db/id 24},
|
||||
:block/anchor "level_1",
|
||||
:block/children
|
||||
({:block/uuid #uuid "60643869-c9dc-4a06-b1c3-e86bad631b3a",
|
||||
:block/left {:db/id 25},
|
||||
:block/anchor "level_1_1",
|
||||
:block/children
|
||||
({:block/uuid #uuid "60643869-3d80-4926-8854-5de911fb2aca",
|
||||
:block/left {:db/id 26},
|
||||
:block/anchor "level_1_1_1",
|
||||
:block/body [],
|
||||
:block/format :markdown,
|
||||
:block/level 4,
|
||||
:block/title [["Plain" "level 1-1-1"]],
|
||||
:block/refs-with-children (),
|
||||
:block/content "#### level 1-1-1\n",
|
||||
:db/id 27,
|
||||
:block/parent {:db/id 26},
|
||||
:block/page {:db/id 20},
|
||||
:block/file {:db/id 16}}),
|
||||
:block/body [],
|
||||
:block/format :markdown,
|
||||
:block/level 3,
|
||||
:block/title [["Plain" "level 1-1"]],
|
||||
:block/refs-with-children (),
|
||||
:block/content "### level 1-1",
|
||||
:db/id 26,
|
||||
:block/parent {:db/id 25},
|
||||
:block/page {:db/id 20},
|
||||
:block/file {:db/id 16}}),
|
||||
:block/body [],
|
||||
:block/format :markdown,
|
||||
:block/level 2,
|
||||
:block/title [["Plain" "level 1"]],
|
||||
:block/refs-with-children (),
|
||||
:block/content "## level 1\n",
|
||||
:db/id 25,
|
||||
:block/parent {:db/id 20},
|
||||
:block/page {:db/id 20},
|
||||
:block/file {:db/id 16}}
|
||||
{:block/uuid #uuid "60643869-39d5-497e-b300-fa49993f6fda",
|
||||
:block/left {:db/id 25},
|
||||
:block/anchor "level_2",
|
||||
:block/repo "logseq_local_test_navtive_fs",
|
||||
:block/body [],
|
||||
:block/format :markdown,
|
||||
:block/level 2,
|
||||
:block/title [["Plain" "level 2"]],
|
||||
:block/refs-with-children (),
|
||||
:block/content "## level 2",
|
||||
:db/id 28,
|
||||
:block/parent {:db/id 20},
|
||||
:block/page {:db/id 20},
|
||||
:block/file {:db/id 16}}))
|
||||
|
||||
(defn- clip-first-space [s]
|
||||
(str/replace s #"\n\s+" "\n"))
|
||||
|
||||
(deftest test-tree->file-content
|
||||
(let [r "---\ntitle: Mar 31th, 2021\n---\n## level 1\n### level 1-1\n#### level 1-1-1\n## level 2"
|
||||
r (clip-first-space r)]
|
||||
(is (= r (file/tree->file-content tree 2)))))
|
||||
|
||||
(comment
|
||||
(run-test test-tree->file-content))
|
Loading…
Reference in New Issue