From f3d926d34478ec12924c010308829fc52e84c3e7 Mon Sep 17 00:00:00 2001 From: rcmerci Date: Tue, 20 Jan 2026 19:21:26 +0800 Subject: [PATCH] impl 009-cli-add-pos-show-tree-align.md --- docs/cli/logseq-cli.md | 12 ++-- src/main/logseq/cli/command/add.cljs | 69 ++++++++++++++++++----- src/main/logseq/cli/command/move.cljs | 18 +++--- src/main/logseq/cli/command/show.cljs | 21 +++++-- src/main/logseq/cli/commands.cljs | 5 +- src/test/logseq/cli/commands_test.cljs | 41 +++++++++++++- src/test/logseq/cli/integration_test.cljs | 40 +++++++++++-- 7 files changed, 163 insertions(+), 43 deletions(-) diff --git a/docs/cli/logseq-cli.md b/docs/cli/logseq-cli.md index b4b074f417..4caa1601ec 100644 --- a/docs/cli/logseq-cli.md +++ b/docs/cli/logseq-cli.md @@ -63,11 +63,11 @@ Inspect and edit commands: - `list page [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list pages - `list tag [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list tags - `list property [--expand] [--limit ] [--offset ] [--sort ] [--order asc|desc]` - list properties -- `add block --content [--page ] [--parent ]` - add blocks; defaults to today’s journal page if no page is given -- `add block --blocks [--page ] [--parent ]` - insert blocks via EDN vector -- `add block --blocks-file [--page ] [--parent ]` - insert blocks from an EDN file +- `add block --content [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - add blocks; defaults to today’s journal page if no target is given +- `add block --blocks [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks via EDN vector +- `add block --blocks-file [--target-page-name |--target-id |--target-uuid ] [--pos first-child|last-child|sibling]` - insert blocks from an EDN file - `add page --page ` - create a page -- `move --id |--uuid --target-id |--target-uuid |--page-name [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child) +- `move --id |--uuid --target-id |--target-uuid |--target-page-name [--pos first-child|last-child|sibling]` - move a block and its children (defaults to first-child) - `remove block --block ` - remove a block and its children - `remove page --page ` - remove a page and its children - `search --text [--type page|block|tag|property|all] [--include-content] [--limit ]` - search across pages, blocks, tags, and properties @@ -117,8 +117,8 @@ Examples: node ./static/logseq-cli.js graph create --repo demo node ./static/logseq-cli.js graph export --type edn --output /tmp/demo.edn --repo demo node ./static/logseq-cli.js graph import --type edn --input /tmp/demo.edn --repo demo-import -node ./static/logseq-cli.js add block --page TestPage --content "hello world" -node ./static/logseq-cli.js move --uuid --page-name TargetPage +node ./static/logseq-cli.js add block --target-page-name TestPage --content "hello world" +node ./static/logseq-cli.js move --uuid --target-page-name TargetPage node ./static/logseq-cli.js search --text "hello" node ./static/logseq-cli.js show --page-name TestPage --format json --output json node ./static/logseq-cli.js server list diff --git a/src/main/logseq/cli/command/add.cljs b/src/main/logseq/cli/command/add.cljs index 4569a1eaf2..f9c31566f7 100644 --- a/src/main/logseq/cli/command/add.cljs +++ b/src/main/logseq/cli/command/add.cljs @@ -15,8 +15,11 @@ {:content {:desc "Block content for add"} :blocks {:desc "EDN vector of blocks for add"} :blocks-file {:desc "EDN file of blocks for add"} - :page {:desc "Page name"} - :parent {:desc "Parent block UUID for add"}}) + :target-id {:desc "Target block db/id" + :coerce :long} + :target-uuid {:desc "Target block UUID"} + :target-page-name {:desc "Target page name"} + :pos {:desc "Position (first-child, last-child, sibling)"}}) (def ^:private add-page-spec {:page {:desc "Page name"}}) @@ -44,17 +47,50 @@ (transport/invoke config :thread-api/pull false [repo [:db/id :block/uuid :block/name :block/title] [:block/name page-name]]))))) +(def ^:private add-positions + #{"first-child" "last-child" "sibling"}) + +(defn invalid-options? + [opts] + (let [pos (some-> (:pos opts) string/trim string/lower-case) + target-id (:target-id opts) + target-uuid (some-> (:target-uuid opts) string/trim) + target-page (some-> (:target-page-name opts) string/trim) + target-selectors (filter some? [target-id target-uuid target-page])] + (cond + (and (seq pos) (not (contains? add-positions pos))) + (str "invalid pos: " (:pos opts)) + + (> (count target-selectors) 1) + "only one of --target-id, --target-uuid, or --target-page-name is allowed" + + (and (= pos "sibling") (or (seq target-page) (empty? target-selectors))) + "--pos sibling is only valid for block targets" + + :else + nil))) + (defn- resolve-add-target - [config {:keys [repo page parent]}] - (if (seq parent) - (if-not (common-util/uuid-string? parent) - (p/rejected (ex-info "parent must be a uuid" {:code :invalid-parent})) + [config {:keys [repo target-id target-uuid target-page-name]}] + (cond + (some? target-id) + (p/let [block (transport/invoke config :thread-api/pull false + [repo [:db/id :block/uuid :block/title] target-id])] + (if-let [id (:db/id block)] + id + (throw (ex-info "target block not found" {:code :target-not-found})))) + + (seq target-uuid) + (if-not (common-util/uuid-string? target-uuid) + (p/rejected (ex-info "target must be a uuid" {:code :invalid-target})) (p/let [block (transport/invoke config :thread-api/pull false - [repo [:db/id :block/uuid :block/title] [:block/uuid (uuid parent)]])] + [repo [:db/id :block/uuid :block/title] [:block/uuid (uuid target-uuid)]])] (if-let [id (:db/id block)] id - (throw (ex-info "parent block not found" {:code :parent-not-found}))))) - (p/let [page-name (if (seq page) page (today-page-title config repo)) + (throw (ex-info "target block not found" {:code :target-not-found}))))) + + :else + (p/let [page-name (if (seq target-page-name) target-page-name (today-page-title config repo)) page-entity (ensure-page! config repo page-name)] (or (:db/id page-entity) (throw (ex-info "page not found" {:code :page-not-found})))))) @@ -104,8 +140,10 @@ :action {:type :add-block :repo repo :graph (core/repo->graph repo) - :page (:page options) - :parent (:parent options) + :target-id (:target-id options) + :target-uuid (some-> (:target-uuid options) string/trim) + :target-page-name (some-> (:target-page-name options) string/trim) + :pos (or (some-> (:pos options) string/trim string/lower-case) "last-child") :blocks (:value vector-result)}})))))) (defn build-add-page-action @@ -129,11 +167,14 @@ [action config] (-> (p/let [cfg (cli-server/ensure-server! config (:repo action)) target-id (resolve-add-target cfg action) + pos (:pos action) + opts (case pos + "last-child" {:sibling? false :bottom? true} + "sibling" {:sibling? true} + {:sibling? false}) ops [[:insert-blocks [(:blocks action) target-id - {:sibling? false - :bottom? true - :outliner-op :insert-blocks}]]] + (assoc opts :outliner-op :insert-blocks)]]] result (transport/invoke cfg :thread-api/apply-outliner-ops false [(:repo action) ops {}])] {:status :ok :data {:result result}}))) diff --git a/src/main/logseq/cli/command/move.cljs b/src/main/logseq/cli/command/move.cljs index 4f57d0bc97..60453ea61a 100644 --- a/src/main/logseq/cli/command/move.cljs +++ b/src/main/logseq/cli/command/move.cljs @@ -14,7 +14,7 @@ :target-id {:desc "Target block db/id" :coerce :long} :target-uuid {:desc "Target block UUID"} - :page-name {:desc "Target page name"} + :target-page-name {:desc "Target page name"} :pos {:desc "Position (first-child, last-child, sibling)"}}) (def entries @@ -29,7 +29,7 @@ source-selectors (filter some? [(:id opts) (some-> (:uuid opts) string/trim)]) target-selectors (filter some? [(:target-id opts) (:target-uuid opts) - (some-> (:page-name opts) string/trim)])] + (some-> (:target-page-name opts) string/trim)])] (cond (and (seq pos) (not (contains? move-positions pos))) (str "invalid pos: " (:pos opts)) @@ -38,9 +38,9 @@ "only one of --id or --uuid is allowed" (> (count target-selectors) 1) - "only one of --target-id, --target-uuid, or --page-name is allowed" + "only one of --target-id, --target-uuid, or --target-page-name is allowed" - (and (= pos "sibling") (seq (some-> (:page-name opts) string/trim))) + (and (= pos "sibling") (seq (some-> (:target-page-name opts) string/trim))) "--pos sibling is only valid for block targets" :else @@ -86,7 +86,7 @@ (p/rejected (ex-info "source is required" {:code :missing-source})))) (defn- resolve-target - [config repo {:keys [target-id target-uuid page-name]}] + [config repo {:keys [target-id target-uuid target-page-name]}] (cond (some? target-id) (p/let [entity (transport/invoke config :thread-api/pull false @@ -103,10 +103,10 @@ (ensure-non-page entity "target must be a block" :invalid-target) (throw (ex-info "target block not found" {:code :target-not-found}))))) - (seq page-name) + (seq target-page-name) (p/let [entity (transport/invoke config :thread-api/pull false [repo [:db/id :block/uuid :block/name :block/title] - [:block/name page-name]])] + [:block/name target-page-name]])] (if (:db/id entity) entity (throw (ex-info "page not found" {:code :page-not-found})))) @@ -132,7 +132,7 @@ uuid (some-> (:uuid options) string/trim) target-id (:target-id options) target-uuid (some-> (:target-uuid options) string/trim) - page-name (some-> (:page-name options) string/trim) + page-name (some-> (:target-page-name options) string/trim) pos (some-> (:pos options) string/trim string/lower-case) source-label (cond (seq uuid) uuid @@ -163,7 +163,7 @@ :uuid uuid :target-id target-id :target-uuid target-uuid - :page-name page-name + :target-page-name page-name :pos (or pos "first-child") :source source-label :target target-label}})))) diff --git a/src/main/logseq/cli/command/show.cljs b/src/main/logseq/cli/command/show.cljs index 85ed029851..d738bb3a0e 100644 --- a/src/main/logseq/cli/command/show.cljs +++ b/src/main/logseq/cli/command/show.cljs @@ -119,8 +119,17 @@ (or (:block/title node) (:block/name node) (str (:block/uuid node)))) node-id (fn [node] (or (:db/id node) "-")) - id-padding (fn [node] - (apply str (repeat (inc (count (str (node-id node)))) " "))) + collect-nodes (fn collect-nodes [node] + (if-let [children (:block/children node)] + (into [node] (mapcat collect-nodes children)) + [node])) + nodes (collect-nodes root) + id-width (apply max (map (fn [node] (count (str (node-id node)))) nodes)) + pad-id (fn [node] + (let [id-str (str (node-id node)) + padding (max 0 (- id-width (count id-str)))] + (str id-str (apply str (repeat padding " "))))) + id-padding (apply str (repeat (inc id-width) " ")) split-lines (fn [value] (string/split (or value "") #"\n")) lines (atom []) @@ -134,17 +143,17 @@ rows (split-lines (label child)) first-row (first rows) rest-rows (rest rows) - line (str (node-id child) " " prefix branch first-row)] + line (str (pad-id child) " " prefix branch first-row)] (swap! lines conj line) (doseq [row rest-rows] - (swap! lines conj (str (id-padding child) next-prefix row))) + (swap! lines conj (str id-padding next-prefix row))) (walk child next-prefix)))))] (let [rows (split-lines (label root)) first-row (first rows) rest-rows (rest rows)] - (swap! lines conj (str (node-id root) " " first-row)) + (swap! lines conj (str (pad-id root) " " first-row)) (doseq [row rest-rows] - (swap! lines conj (str (id-padding root) row)))) + (swap! lines conj (str id-padding row)))) (walk root "") (string/join "\n" @lines))) diff --git a/src/main/logseq/cli/commands.cljs b/src/main/logseq/cli/commands.cljs index 7c1d6aa3b5..1da9b0e9ff 100644 --- a/src/main/logseq/cli/commands.cljs +++ b/src/main/logseq/cli/commands.cljs @@ -125,7 +125,7 @@ move-sources (filter some? [(:id opts) (some-> (:uuid opts) string/trim)]) move-targets (filter some? [(:target-id opts) (some-> (:target-uuid opts) string/trim) - (some-> (:page-name opts) string/trim)])] + (some-> (:target-page-name opts) string/trim)])] (cond (:help opts) (command-core/help-result cmd-summary) @@ -137,6 +137,9 @@ (and (= command :add-block) (not has-content?)) (missing-content-result summary) + (and (= command :add-block) (add-command/invalid-options? opts)) + (command-core/invalid-options-result summary (add-command/invalid-options? opts)) + (and (= command :add-page) (not (seq (:page opts)))) (missing-page-name-result summary) diff --git a/src/test/logseq/cli/commands_test.cljs b/src/test/logseq/cli/commands_test.cljs index 7bbb287748..de8a888ee5 100644 --- a/src/test/logseq/cli/commands_test.cljs +++ b/src/test/logseq/cli/commands_test.cljs @@ -166,6 +166,23 @@ "4 └── Child B") (tree->text tree-data)))))) +(deftest test-tree->text-aligns-mixed-id-widths + (testing "show tree text aligns glyph column with mixed-width ids" + (let [tree->text #'show-command/tree->text + tree-data {:root {:db/id 7 + :block/title "Root" + :block/children [{:db/id 88 + :block/title "Child A" + :block/children [{:db/id 3 + :block/title "Grand"}]} + {:db/id 1000 + :block/title "Child B"}]}}] + (is (= (str "7 Root\n" + "88 ├── Child A\n" + "3 │ └── Grand\n" + "1000 └── Child B") + (tree->text tree-data)))))) + (deftest test-tree->text-multiline (testing "show tree text renders multiline blocks under glyph column" (let [tree->text #'show-command/tree->text @@ -272,6 +289,23 @@ (is (= :add-block (:command result))) (is (= "hello" (get-in result [:options :content]))))) + (testing "add block parses with target selectors and pos" + (let [result (commands/parse-args ["add" "block" + "--content" "hello" + "--target-uuid" "abc" + "--pos" "first-child"])] + (is (true? (:ok? result))) + (is (= :add-block (:command result))) + (is (= "abc" (get-in result [:options :target-uuid]))) + (is (= "first-child" (get-in result [:options :pos]))))) + + (testing "add block rejects invalid pos" + (let [result (commands/parse-args ["add" "block" + "--content" "hello" + "--pos" "middle"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + (testing "add page requires page name" (let [result (commands/parse-args ["add" "page"])] (is (false? (:ok? result))) @@ -537,7 +571,12 @@ (is (= :invalid-options (get-in result [:error :code]))))) (testing "move rejects sibling pos for page target" - (let [result (commands/parse-args ["move" "--id" "1" "--page-name" "Home" "--pos" "sibling"])] + (let [result (commands/parse-args ["move" "--id" "1" "--target-page-name" "Home" "--pos" "sibling"])] + (is (false? (:ok? result))) + (is (= :invalid-options (get-in result [:error :code]))))) + + (testing "move rejects legacy page-name option" + (let [result (commands/parse-args ["move" "--id" "1" "--page-name" "Home"])] (is (false? (:ok? result))) (is (= :invalid-options (get-in result [:error :code])))))) diff --git a/src/test/logseq/cli/integration_test.cljs b/src/test/logseq/cli/integration_test.cljs index 82e8feb84c..ef5941fe0d 100644 --- a/src/test/logseq/cli/integration_test.cljs +++ b/src/test/logseq/cli/integration_test.cljs @@ -96,7 +96,7 @@ list-tag-payload (parse-json-output list-tag-result) list-property-result (run-cli ["--repo" "content-graph" "list" "property"] data-dir cfg-path) list-property-payload (parse-json-output list-property-result) - add-block-result (run-cli ["--repo" "content-graph" "add" "block" "--page" "TestPage" "--content" "hello world"] data-dir cfg-path) + add-block-result (run-cli ["--repo" "content-graph" "add" "block" "--target-page-name" "TestPage" "--content" "hello world"] data-dir cfg-path) _ (parse-json-output add-block-result) search-result (run-cli ["--repo" "content-graph" "search" "--text" "hello world"] data-dir cfg-path) search-payload (parse-json-output search-result) @@ -133,13 +133,13 @@ _ (run-cli ["graph" "create" "--repo" "move-graph"] data-dir cfg-path) _ (run-cli ["--repo" "move-graph" "add" "page" "--page" "SourcePage"] data-dir cfg-path) _ (run-cli ["--repo" "move-graph" "add" "page" "--page" "TargetPage"] data-dir cfg-path) - _ (run-cli ["--repo" "move-graph" "add" "block" "--page" "SourcePage" "--content" "Parent Block"] data-dir cfg-path) + _ (run-cli ["--repo" "move-graph" "add" "block" "--target-page-name" "SourcePage" "--content" "Parent Block"] data-dir cfg-path) source-show (run-cli ["--repo" "move-graph" "show" "--page-name" "SourcePage" "--format" "json"] data-dir cfg-path) source-payload (parse-json-output source-show) parent-node (find-block-by-title (get-in source-payload [:data :root]) "Parent Block") parent-uuid (or (:block/uuid parent-node) (:uuid parent-node)) - _ (run-cli ["--repo" "move-graph" "add" "block" "--parent" (str parent-uuid) "--content" "Child Block"] data-dir cfg-path) - move-result (run-cli ["--repo" "move-graph" "move" "--uuid" (str parent-uuid) "--page-name" "TargetPage"] data-dir cfg-path) + _ (run-cli ["--repo" "move-graph" "add" "block" "--target-uuid" (str parent-uuid) "--content" "Child Block"] data-dir cfg-path) + move-result (run-cli ["--repo" "move-graph" "move" "--uuid" (str parent-uuid) "--target-page-name" "TargetPage"] data-dir cfg-path) move-payload (parse-json-output move-result) target-show (run-cli ["--repo" "move-graph" "show" "--page-name" "TargetPage" "--format" "json"] data-dir cfg-path) target-payload (parse-json-output target-show) @@ -157,6 +157,34 @@ (is false (str "unexpected error: " e)) (done))))))) +(deftest test-cli-add-block-pos-ordering + (async done + (let [data-dir (node-helper/create-tmp-dir "db-worker-add-pos")] + (-> (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" "--repo" "add-pos-graph"] data-dir cfg-path) + _ (run-cli ["--repo" "add-pos-graph" "add" "page" "--page" "PosPage"] data-dir cfg-path) + _ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-page-name" "PosPage" "--content" "Parent"] data-dir cfg-path) + parent-show (run-cli ["--repo" "add-pos-graph" "show" "--page-name" "PosPage" "--format" "json"] data-dir cfg-path) + parent-payload (parse-json-output parent-show) + parent-node (find-block-by-title (get-in parent-payload [:data :root]) "Parent") + parent-uuid (or (:block/uuid parent-node) (:uuid parent-node)) + _ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-uuid" (str parent-uuid) "--pos" "first-child" "--content" "First"] data-dir cfg-path) + _ (run-cli ["--repo" "add-pos-graph" "add" "block" "--target-uuid" (str parent-uuid) "--pos" "last-child" "--content" "Last"] data-dir cfg-path) + final-show (run-cli ["--repo" "add-pos-graph" "show" "--page-name" "PosPage" "--format" "json"] data-dir cfg-path) + final-payload (parse-json-output final-show) + final-parent (find-block-by-title (get-in final-payload [:data :root]) "Parent") + child-titles (map node-title (node-children final-parent)) + stop-result (run-cli ["server" "stop" "--repo" "add-pos-graph"] data-dir cfg-path) + stop-payload (parse-json-output stop-result)] + (is (some? parent-uuid)) + (is (= ["First" "Last"] (vec child-titles))) + (is (= "ok" (:status stop-payload))) + (done)) + (p/catch (fn [e] + (is false (str "unexpected error: " e)) + (done))))))) + (deftest test-cli-output-formats-graph-list (async done (let [data-dir (node-helper/create-tmp-dir "db-worker")] @@ -268,7 +296,7 @@ export-path (node-path/join (node-helper/create-tmp-dir "exports") "graph.edn") _ (run-cli ["graph" "create" "--repo" export-graph] data-dir cfg-path) _ (run-cli ["--repo" export-graph "add" "page" "--page" "ExportPage"] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "block" "--page" "ExportPage" "--content" "Export content"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "add" "block" "--target-page-name" "ExportPage" "--content" "Export content"] data-dir cfg-path) export-result (run-cli ["--repo" export-graph "graph" "export" "--type" "edn" @@ -307,7 +335,7 @@ export-path (node-path/join (node-helper/create-tmp-dir "exports") "graph.sqlite") _ (run-cli ["graph" "create" "--repo" export-graph] data-dir cfg-path) _ (run-cli ["--repo" export-graph "add" "page" "--page" "SQLiteExportPage"] data-dir cfg-path) - _ (run-cli ["--repo" export-graph "add" "block" "--page" "SQLiteExportPage" "--content" "SQLite export content"] data-dir cfg-path) + _ (run-cli ["--repo" export-graph "add" "block" "--target-page-name" "SQLiteExportPage" "--content" "SQLite export content"] data-dir cfg-path) export-result (run-cli ["--repo" export-graph "graph" "export" "--type" "sqlite"