mirror of
https://github.com/logseq/logseq.git
synced 2026-05-15 00:12:35 +00:00
enhance: 'upsert block' support update --content and --status
This commit is contained in:
@@ -182,7 +182,8 @@ Inspect and edit commands:
|
||||
- `upsert block --content <text> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - create blocks; defaults to today’s journal page if no target is given
|
||||
- `upsert block --blocks <edn> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector
|
||||
- `upsert block --blocks-file <path> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file
|
||||
- `upsert block --id <id>|--uuid <uuid> [--target-id <id>|--target-uuid <uuid>|--target-page <name>] [--pos first-child|last-child|sibling] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - update and/or move a block
|
||||
- `upsert block --id <id>|--uuid <uuid> [--content <text>] [--status <status>] [--target-id <id>|--target-uuid <uuid>|--target-page <name>] [--pos first-child|last-child|sibling] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - update and/or move a block
|
||||
- When both `--status` and `--update-properties` set `:logseq.property/status`, the value from `--update-properties` takes precedence.
|
||||
- `upsert page --page <name> [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - create (or update by page name) a page
|
||||
- `upsert page --id <id> [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - update a page by id (cannot be combined with `--page`)
|
||||
- `upsert tag --name <name>` - create or upsert a tag by name
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"in progress" :logseq.property/status.doing
|
||||
"inprogress" :logseq.property/status.doing})
|
||||
|
||||
(defn- normalize-status
|
||||
(defn normalize-status
|
||||
[value]
|
||||
(let [text (some-> value string/trim)
|
||||
parsed (when (and (seq text) (string/starts-with? text ":"))
|
||||
|
||||
@@ -22,7 +22,14 @@
|
||||
has-update-properties? (seq (some-> (:update-properties opts) string/trim))
|
||||
has-remove-tags? (seq (some-> (:remove-tags opts) string/trim))
|
||||
has-remove-properties? (seq (some-> (:remove-properties opts) string/trim))
|
||||
has-updates? (or has-update-tags? has-update-properties? has-remove-tags? has-remove-properties?)]
|
||||
has-status? (seq (some-> (:status opts) string/trim))
|
||||
has-content? (contains? opts :content)
|
||||
has-updates? (or has-update-tags?
|
||||
has-update-properties?
|
||||
has-remove-tags?
|
||||
has-remove-properties?
|
||||
has-status?
|
||||
has-content?)]
|
||||
(cond
|
||||
(and (seq pos) (not (contains? update-positions pos)))
|
||||
(str "invalid pos: " (:pos opts))
|
||||
@@ -134,6 +141,12 @@
|
||||
target-uuid (some-> (:target-uuid options) string/trim)
|
||||
page-name (some-> (:target-page options) string/trim)
|
||||
pos (some-> (:pos options) string/trim string/lower-case)
|
||||
status-provided? (contains? options :status)
|
||||
status-text (some-> (:status options) string/trim)
|
||||
status (when (seq status-text)
|
||||
(add-command/normalize-status status-text))
|
||||
content-provided? (contains? options :content)
|
||||
content (:content options)
|
||||
update-tags-result (add-command/parse-tags-option (:update-tags options))
|
||||
update-properties-result (add-command/parse-properties-option
|
||||
(:update-properties options)
|
||||
@@ -144,10 +157,19 @@
|
||||
{:allow-non-built-in? true})
|
||||
update-tags (:value update-tags-result)
|
||||
update-properties (:value update-properties-result)
|
||||
update-properties (merge (cond-> {}
|
||||
status
|
||||
(assoc :logseq.property/status status))
|
||||
(or update-properties {}))
|
||||
remove-tags (:value remove-tags-result)
|
||||
remove-properties (:value remove-properties-result)
|
||||
has-target? (or (some? target-id) (seq target-uuid) (seq page-name))
|
||||
has-updates? (or (seq update-tags) (seq update-properties) (seq remove-tags) (seq remove-properties))
|
||||
has-updates? (or (seq update-tags)
|
||||
(seq update-properties)
|
||||
(seq remove-tags)
|
||||
(seq remove-properties)
|
||||
status-provided?
|
||||
content-provided?)
|
||||
source-label (cond
|
||||
(seq uuid) uuid
|
||||
(some? id) (str id)
|
||||
@@ -163,6 +185,11 @@
|
||||
:error {:code :missing-source
|
||||
:message "source block is required"}}
|
||||
|
||||
(and status-provided? (not status))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (str "invalid status: " (:status options))}}
|
||||
|
||||
(and (not has-target?) (not has-updates?))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
@@ -182,21 +209,22 @@
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action {:type :update-block
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id id
|
||||
:uuid uuid
|
||||
:target-id target-id
|
||||
:target-uuid target-uuid
|
||||
:target-page page-name
|
||||
:pos (when has-target? (or pos "first-child"))
|
||||
:update-tags update-tags
|
||||
:update-properties update-properties
|
||||
:remove-tags remove-tags
|
||||
:remove-properties remove-properties
|
||||
:source source-label
|
||||
:target target-label}}))))
|
||||
:action (cond-> {:type :update-block
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)
|
||||
:id id
|
||||
:uuid uuid
|
||||
:target-id target-id
|
||||
:target-uuid target-uuid
|
||||
:target-page page-name
|
||||
:pos (when has-target? (or pos "first-child"))
|
||||
:update-tags update-tags
|
||||
:update-properties update-properties
|
||||
:remove-tags remove-tags
|
||||
:remove-properties remove-properties
|
||||
:source source-label
|
||||
:target target-label}
|
||||
content-provided? (assoc :content content))}))))
|
||||
|
||||
(defn execute-update
|
||||
[action config]
|
||||
@@ -217,12 +245,17 @@
|
||||
{:allow-non-built-in? true})
|
||||
block-id (:db/id source)
|
||||
block-ids [block-id]
|
||||
content-provided? (contains? action :content)
|
||||
content (:content action)
|
||||
update-tag-ids (when (seq update-tags)
|
||||
(->> update-tags (map :db/id) (remove nil?) vec))
|
||||
remove-tag-ids (when (seq remove-tags)
|
||||
(->> remove-tags (map :db/id) (remove nil?) vec))
|
||||
ops (cond-> []
|
||||
target (conj [:move-blocks [[(:db/id source)] (:db/id target) opts]]))
|
||||
content-provided?
|
||||
(conj [:save-block [{:db/id block-id :block/title content} {}]])
|
||||
target
|
||||
(conj [:move-blocks [[(:db/id source)] (:db/id target) opts]]))
|
||||
ops (cond-> ops
|
||||
(seq remove-tag-ids)
|
||||
(into (map (fn [tag-id]
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
:complete :pages}
|
||||
:pos {:desc "Position. Default: create=last-child, update=first-child"
|
||||
:values ["first-child" "last-child" "sibling"]}
|
||||
:content {:desc "Block content for create mode"}
|
||||
:content {:desc "Block content (create inserts; update rewrites source block content)"}
|
||||
:blocks {:desc "EDN vector of blocks for create mode"}
|
||||
:blocks-file {:desc "EDN file of blocks for create mode"
|
||||
:complete :file}
|
||||
:status {:desc "Task status"
|
||||
:status {:desc "Task status (create/update)"
|
||||
:values ["todo" "doing" "done" "now" "later" "wait" "waiting"
|
||||
"backlog" "canceled" "cancelled" "in-review" "in-progress"]}
|
||||
:update-tags {:desc "Tags to add/update (EDN vector)"}
|
||||
|
||||
@@ -1029,6 +1029,22 @@
|
||||
(is (= :upsert-block (:command result)))
|
||||
(is (= "abc" (get-in result [:options :uuid])))))
|
||||
|
||||
(testing "upsert block update mode accepts status-only updates"
|
||||
(let [result (commands/parse-args ["upsert" "block" "--id" "1"
|
||||
"--status" "done"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-block (:command result)))
|
||||
(is (= 1 (get-in result [:options :id])))
|
||||
(is (= "done" (get-in result [:options :status])))))
|
||||
|
||||
(testing "upsert block update mode accepts content-only updates"
|
||||
(let [result (commands/parse-args ["upsert" "block" "--id" "1"
|
||||
"--content" "updated text"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-block (:command result)))
|
||||
(is (= 1 (get-in result [:options :id])))
|
||||
(is (= "updated text" (get-in result [:options :content])))))
|
||||
|
||||
(testing "upsert block forces update mode when id and content are both provided"
|
||||
(let [result (commands/parse-args ["upsert" "block"
|
||||
"--id" "1"
|
||||
@@ -1623,6 +1639,44 @@
|
||||
(is (= :upsert-block (get-in result [:action :type])))
|
||||
(is (= ["TagA"] (get-in result [:action :update-tags])))))
|
||||
|
||||
(testing "update accepts status-only updates"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-block
|
||||
:options {:id 1 :status "in-progress"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-block (get-in result [:action :type])))
|
||||
(is (= :logseq.property/status.doing
|
||||
(get-in result [:action :update-properties :logseq.property/status])))))
|
||||
|
||||
(testing "update accepts content-only updates"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-block
|
||||
:options {:id 1 :content "updated text"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-block (get-in result [:action :type])))
|
||||
(is (= "updated text" (get-in result [:action :content])))))
|
||||
|
||||
(testing "update rejects invalid status"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-block
|
||||
:options {:id 1 :status "invalid-status"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "update properties status takes precedence over --status"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-block
|
||||
:options {:id 1
|
||||
:status "doing"
|
||||
:update-properties "{:logseq.property/status :logseq.property/status.done}"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :logseq.property/status.done
|
||||
(get-in result [:action :update-properties :logseq.property/status])))))
|
||||
|
||||
(testing "update rejects invalid update tags"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-block
|
||||
@@ -2200,9 +2254,11 @@
|
||||
(let [ops* (atom nil)
|
||||
calls* (atom [])
|
||||
action {:type :upsert-block :mode :update :repo "demo" :id 1 :target-id 2 :pos "last-child"
|
||||
:content "Updated heading"
|
||||
:update-tags [:tag/new]
|
||||
:remove-tags [:tag/old]
|
||||
:update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"}
|
||||
:update-properties {:logseq.property/deadline "2026-01-25T12:00:00Z"
|
||||
:logseq.property/status :logseq.property/status.done}
|
||||
:remove-properties [:logseq.property/publishing-public?]}]
|
||||
(-> (p/with-redefs [cli-server/list-graphs (fn [_] ["demo"])
|
||||
cli-server/ensure-server! (fn [_ _] {:base-url "http://example"})
|
||||
@@ -2226,11 +2282,13 @@
|
||||
(throw (ex-info "unexpected invoke" {:method method :calls @calls*}))))]
|
||||
(p/let [result (commands/execute action {})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= [[:move-blocks [[1] 2 {:sibling? false :bottom? true}]]
|
||||
(is (= [[:save-block [{:db/id 1 :block/title "Updated heading"} {}]]
|
||||
[:move-blocks [[1] 2 {:sibling? false :bottom? true}]]
|
||||
[:batch-delete-property-value [[1] :block/tags 202]]
|
||||
[:batch-remove-property [[1] :logseq.property/publishing-public?]]
|
||||
[:batch-set-property [[1] :block/tags 101 {}]]
|
||||
[:batch-set-property [[1] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]]]
|
||||
[:batch-set-property [[1] :logseq.property/deadline "2026-01-25T12:00:00Z" {}]]
|
||||
[:batch-set-property [[1] :logseq.property/status :logseq.property/status.done {}]]]
|
||||
@ops*))))
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e " calls: " @calls*))))
|
||||
(p/finally done)))))
|
||||
|
||||
@@ -2043,6 +2043,54 @@
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-upsert-block-update-content-and-status
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-update-content-status")]
|
||||
(-> (p/let [cfg-path (node-path/join (node-helper/create-tmp-dir "cli") "cli.edn")
|
||||
_ (fs/writeFileSync cfg-path "{:output-format :json}")
|
||||
_ (run-cli ["graph" "create" "--graph" "update-content-status-graph"] data-dir cfg-path)
|
||||
_ (run-cli ["--graph" "update-content-status-graph" "upsert" "page" "--page" "Tasks"] data-dir cfg-path)
|
||||
create-result (run-cli ["--graph" "update-content-status-graph"
|
||||
"upsert" "block"
|
||||
"--target-page" "Tasks"
|
||||
"--content" "Task Seed"
|
||||
"--status" "todo"]
|
||||
data-dir cfg-path)
|
||||
create-payload (parse-json-output create-result)
|
||||
block-id (first-result-id create-payload)
|
||||
update-result (run-cli ["--graph" "update-content-status-graph"
|
||||
"upsert" "block"
|
||||
"--id" (str block-id)
|
||||
"--content" "Task Updated"
|
||||
"--status" "done"]
|
||||
data-dir cfg-path)
|
||||
update-payload (parse-json-output update-result)
|
||||
_ (p/delay 100)
|
||||
query-title-result (run-cli ["--graph" "update-content-status-graph"
|
||||
"query"
|
||||
"--query" "[:find ?title . :in $ ?id :where [?id :block/title ?title]]"
|
||||
"--inputs" (pr-str [block-id])]
|
||||
data-dir cfg-path)
|
||||
query-title-payload (parse-json-output query-title-result)
|
||||
query-status-result (run-cli ["--graph" "update-content-status-graph"
|
||||
"query"
|
||||
"--query" "[:find ?ident . :in $ ?id :where [?id :logseq.property/status ?status] [?status :db/ident ?ident]]"
|
||||
"--inputs" (pr-str [block-id])]
|
||||
data-dir cfg-path)
|
||||
query-status-payload (parse-json-output query-status-result)
|
||||
stop-result (run-cli ["server" "stop" "--graph" "update-content-status-graph"] data-dir cfg-path)
|
||||
stop-payload (parse-json-output stop-result)]
|
||||
(is (= "ok" (:status create-payload)))
|
||||
(is (some? block-id))
|
||||
(is (= "ok" (:status update-payload)))
|
||||
(is (= "Task Updated" (get-in query-title-payload [:data :result])))
|
||||
(is (string/includes? (str (get-in query-status-payload [:data :result])) "status.done"))
|
||||
(is (= "ok" (:status stop-payload)))
|
||||
(done))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))
|
||||
(done)))))))
|
||||
|
||||
(deftest ^:long test-cli-add-block-pos-ordering
|
||||
(async done
|
||||
(let [data-dir (node-helper/create-tmp-dir "db-worker-add-pos")]
|
||||
|
||||
Reference in New Issue
Block a user