diff --git a/cli-e2e/spec/non_sync_cases.edn b/cli-e2e/spec/non_sync_cases.edn index 130892edf0..2174da67ab 100644 --- a/cli-e2e/spec/non_sync_cases.edn +++ b/cli-e2e/spec/non_sync_cases.edn @@ -318,6 +318,24 @@ "--order"]}}, :tags [:upsert :list], :extends :non-sync/graph-json-env} + {:id "node-list-renders-block-ref-labels-json", + :cmds + ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert tag --graph {{graph-arg}} --name RefNodeTag >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page RefNodePage >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content \"Task [[$({{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"RefNodePage\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')]]\" --update-tags '[\"RefNodeTag\"]' >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json list node --graph {{graph-arg}} --tags RefNodeTag --fields id,title,type --sort updated-at --order desc --limit 20"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["Task [[RefNodePage]]"]}, + :covers + {:commands ["upsert tag" "upsert page" "upsert block" "list node"], + :options + {:global ["--config" "--graph" "--root-dir" "--output"], + :upsert ["--name" "--page" "--target-page" "--content" "--update-tags"], + :list ["--tags" "--fields" "--limit" "--sort" "--order"]}}, + :tags [:upsert :list], + :extends :non-sync/graph-json-env} {:id "asset-upsert-and-list-json", :setup ["printf 'asset-content' > {{export-path-arg}}.png"], :cmds @@ -480,6 +498,23 @@ :search ["--content"]}}, :tags [:search :smoke], :extends :non-sync/graph-json-env} + {:id "search-block-renders-block-ref-labels-json", + :cmds + ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchRefPage >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert block --graph {{graph-arg}} --target-page Home --content \"foo [[$({{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json query --graph {{graph-arg}} --query '[:find ?uuid . :where [?e :block/title \"SearchRefPage\"] [?e :block/uuid ?uuid]]' | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"data\"][\"result\"])')]]\" >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json search block --graph {{graph-arg}} --content foo"], + :expect + {:exit 0, + :stdout-json-paths {[:status] "ok"}, + :stdout-contains ["foo [[SearchRefPage]]"]}, + :covers + {:commands ["upsert page" "upsert block" "search block"], + :options + {:global ["--config" "--graph" "--root-dir" "--output"], + :upsert ["--page" "--target-page" "--content"], + :search ["--content"]}}, + :tags [:upsert :search], + :extends :non-sync/graph-json-env} {:id "search-page-json", :setup ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page SearchPageTarget >/dev/null"], @@ -595,6 +630,24 @@ ["--id" "--target-page" "--content" "--update-properties"], :show ["--id"]}}, :tags [:upsert :show]} + {:id "show-property-many-ref-default-renders-nested-block-ref-human", + :setup + ["printf '%s' '{:properties {:user.property/reproducible-steps {:block/title \"Reproducible steps\" :logseq.property/type :default :db/cardinality :db.cardinality/many :db/valueType :db.type/ref :logseq.property/public? true}} :pages-and-blocks [{:page {:block/title \"Targets\"} :blocks [{:block/title \"Nested target\" :block/uuid #uuid \"11111111-1111-1111-1111-111111111111\" :build/keep-uuid? true} {:block/title \"Step [[11111111-1111-1111-1111-111111111111]]\" :block/uuid #uuid \"22222222-2222-2222-2222-222222222222\" :build/keep-uuid? true} {:block/title \"Verify output\" :block/uuid #uuid \"33333333-3333-3333-3333-333333333333\" :build/keep-uuid? true}]} {:page {:block/title \"Home\"} :blocks [{:block/title \"Alpha block\" :build/properties {:user.property/reproducible-steps #{[:block/uuid #uuid \"22222222-2222-2222-2222-222222222222\"] [:block/uuid #uuid \"33333333-3333-3333-3333-333333333333\"]}}}]}]}' > {{export-path-arg}}"], + :cmds + ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json graph import --graph {{graph-arg}} --type edn --input {{export-path-arg}} >/dev/null" + "{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output human show --graph {{graph-arg}} --page Home"], + :expect + {:exit 0, + :stdout-contains ["Reproducible steps:" "Step [[Nested target]]" "Verify output"], + :stdout-not-contains ["Step [[11111111-1111-1111-1111-111111111111]]"]}, + :covers + {:commands ["graph import" "show"], + :options + {:global ["--config" "--graph" "--root-dir" "--output"], + :graph ["--type" "--input"], + :show ["--page"]}}, + :tags [:graph :show], + :extends :non-sync/graph-json-env} {:id "block-upsert-update-uuid-json", :setup ["{{cli}} --root-dir {{root-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null" diff --git a/docs/agent-guide/logseq-cli/003-cli-block-ref-rendering.md b/docs/agent-guide/logseq-cli/003-cli-block-ref-rendering.md new file mode 100644 index 0000000000..98e113d2ab --- /dev/null +++ b/docs/agent-guide/logseq-cli/003-cli-block-ref-rendering.md @@ -0,0 +1,302 @@ +# CLI Block Ref Rendering Implementation Plan + +Goal: Make `list`, `search`, and `show` render user-visible `:block/title` text through one shared block-ref replacement path, including `show` property values whose effective text is stored as default string content. + +Architecture: Keep the current command-specific query semantics for `list`, `search`, and `show`, but route every user-visible block-title-like string through one shared CLI helper that replaces `[[block-uuid]]` with stable labels fetched through the same lookup strategy that `show` already uses. +Architecture: Extract the existing `show` UUID replacement logic into a CLI-shared namespace, then reuse it for list and search result normalization and for show-specific tree and property rendering so there is one visible block-ref rendering behavior across commands. + +Tech Stack: ClojureScript, Datascript, `db-worker-node`, Transit transport, CLI formatter code, CLI unit tests, and CLI e2e specs. + +Related: Builds on `docs/agent-guide/logseq-cli/001-logseq-cli.md`. + +## Problem statement + +The CLI currently renders user-visible block title text through multiple independent paths. + +`search block` returns raw `:block/title` values from `src/main/logseq/cli/command/search.cljs` and sends them straight to `src/main/logseq/cli/format.cljs`. + +The `list` family returns raw `:block/title` values from worker helpers in `src/main/logseq/cli/common/db_worker.cljs` and also sends them to `src/main/logseq/cli/format.cljs`. + +The `show` command has its own richer rendering flow in `src/main/logseq/cli/command/show.cljs`, where it extracts UUID refs from visible text, fetches entities for those UUIDs, and rewrites visible strings before printing. + +Because these paths are separate, visible output can diverge. + +The reported bug is one instance of that divergence, where `logseq search block -c foo` can show `foo [[UUID]]` instead of `foo [[bar]]`. + +The same architectural inconsistency also risks divergence in `list` command title columns and in `show` output when block-title-like strings appear in property values. + +A particularly important case is `show` rendering for a block property whose value is effectively default string content, because `property-value->string` currently returns string values directly and does not run them through the same block-ref replacement path used by `block-label` and tree node resolution. + +The implementation should unify visible block-ref rendering behavior across `list`, `search`, and `show` without changing each command family's existing query, filtering, sorting, or transport semantics beyond the additional UUID label lookups needed for rendering. + +## Testing Plan + +Implementation should follow @Test-Driven Development. + +I will add failing unit tests in `src/test/logseq/cli/command/search_test.cljs` that stub raw search results containing `[[block-uuid]]` in `:block/title`, then stub follow-up UUID lookups, and assert that `execute-search-block` returns `foo [[bar]]` instead of the raw UUID form. + +I will add failing unit tests for the list command family in a suitable existing or new list-command test namespace under `src/test/logseq/cli/command/`, and those tests will verify that command execution normalizes user-visible `:block/title` values before they reach the formatter. + +I will add a focused unit test for the shared UUID replacement helper namespace so direct text replacement, multiple refs in one string, and recursive replacement behavior are locked down independently of any one command. + +I will add or update `show` command tests so the extracted helper remains behavior-compatible with current `show` output for node titles. + +I will add a specific `show` regression test for property rendering where a property value is a plain string containing `[[block-uuid]]`, and I will assert that the rendered property line shows the resolved label rather than the raw UUID. + +I will add a second `show` regression for a property map whose visible text comes from `:block/title` or string `:logseq.property/value`, because both of those cases can represent user-visible default string content and both should use the same replacement path. + +I will add formatter regressions in `src/test/logseq/cli/format_test.cljs` that prove normalized list and search titles still satisfy the existing table output contract. + +I will add CLI e2e coverage in `cli-e2e/spec/non_sync_cases.edn` for at least one `search block` scenario and one `list` scenario that both expose a visible `[[page-ref]]` title and verify stdout contains the label form and not the UUID form. + +If there is already an appropriate CLI e2e place for `show`, I will add a case that verifies a property line with string content containing a block ref also renders the label form. + +I will run `bb dev:test -v logseq.cli.command.search-test` after writing the search failing tests and expect the new assertions to fail before implementation. + +I will run the targeted list and show test namespaces after adding their failing tests and expect the new assertions to fail before implementation. + +I will run `bb dev:test -v logseq.cli.format-test` after adding the formatter regressions and expect the new visible-output assertions to fail before implementation if raw UUIDs are still printed. + +I will run `bb -f cli-e2e/bb.edn test --skip-build` after the unit tests pass and expect the new CLI scenarios to pass with human-readable labels. + +NOTE: I will write *all* tests before I add any implementation behavior. + +## Current implementation snapshot + +The current rendering topology is: + +```text +list page|tag|property|task|node|asset + -> src/main/logseq/cli/command/list.cljs + -> transport/invoke :thread-api/cli-list-* + -> src/main/logseq/cli/common/db_worker.cljs returns raw :block/title + -> src/main/logseq/cli/format.cljs TITLE column prints it + +search block|page|property|tag + -> src/main/logseq/cli/command/search.cljs + -> transport/invoke :thread-api/q + -> Datascript pull returns raw :block/title + -> src/main/logseq/cli/format.cljs TITLE column prints it + +show + -> src/main/logseq/cli/command/show.cljs + -> collects visible UUID refs from node text + -> transport/invoke :thread-api/pull for uuid labels + -> replace-uuid-refs / resolve-uuid-refs-in-node + -> tree and block labels render resolved text +``` + +The list formatter path is centralized in `src/main/logseq/cli/format.cljs` through `list-page-columns`, `list-tag-columns`, `list-property-columns`, `list-task-columns`, `list-node-columns`, and `list-asset-columns`. + +Those columns use `:block/title` directly as their visible `TITLE` content. + +The list command data comes from `src/main/logseq/cli/common/db_worker.cljs`, where helpers such as `minimal-list-item` and `minimal-node-item` return raw `:block/title` strings. + +The search command data comes from `src/main/logseq/cli/command/search.cljs`, where `search-block-query` and the other search queries return raw `:block/title` strings and `normalize-items` preserves those strings for output. + +The show command already contains the CLI-side block-ref replacement logic in `src/main/logseq/cli/command/show.cljs`. + +The relevant `show` functions are `extract-uuid-refs`, `collect-uuid-refs`, `fetch-uuid-entities`, `replace-uuid-refs`, `resolve-uuid-refs-in-node`, and `resolve-uuid-refs-in-tree-data`. + +The `show` command also has a separate property rendering path through `property-value->string`, `normalize-property-values`, `node-user-property-entries`, and `node-property-lines`. + +That property rendering path currently treats plain strings as final text and therefore does not inherently share the same block-ref replacement behavior as `block-label`. + +## Recommended approach + +Create one CLI-shared block-ref rendering helper and make every visible `:block/title` rendering path use it. + +Do not keep separate ad hoc text replacement logic in `show`, `list`, and `search`. + +Do not switch `search` or `list` to unrelated worker APIs just to get rendering behavior. + +The preferred shape is: + +```text +visible title-like string + -> collect UUID refs from text + -> fetch uuid labels through shared pull strategy + -> replace UUID refs through shared pure helper + -> hand normalized value to list/search/show formatter logic +``` + +The comparison is: + +| Option | Benefit | Risk | Recommendation | +| --- | --- | --- | --- | +| Extract `show` block-ref replacement helpers and reuse them everywhere visible block-title text is rendered. | One CLI source of truth for visible block-ref rendering. | Requires helper extraction plus per-command lookup integration. | Recommended. | +| Keep separate implementations in `show`, `list`, and `search`. | Lowest local edit count per file. | Guarantees long-term divergence and repeated bugs. | Reject. | +| Reuse lower-level DB/frontend title conversion helpers for some commands and `show` helpers for others. | Short-term convenience. | Still leaves multiple user-visible rendering semantics in the CLI. | Reject. | +| Switch list or search to different worker APIs just to get title rendering. | May reuse existing worker display paths. | Changes query semantics, ranking, or payload contracts. | Reject. | + +## Execution steps + +1. Write the failing shared-helper unit tests first. + +Create or update a test namespace that locks the extracted helper behavior for direct `[[uuid]]` replacement, multiple refs in one string, and replacement stability when an entity label is missing. + +2. Extract the pure block-ref text helpers from `src/main/logseq/cli/command/show.cljs`. + +Move `uuid-ref-pattern`, `extract-uuid-refs`, `replace-uuid-refs-once`, and `replace-uuid-refs` into a CLI-shared namespace such as `src/main/logseq/cli/uuid_refs.cljs`. + +Keep the extracted API small and obviously reusable for both top-level node titles and property values. + +3. Extract or share the UUID entity lookup path that `show` already uses. + +Reuse the `:thread-api/pull` lookup shape currently implemented in `fetch-uuid-entities` with selector `[:db/id :block/uuid :block/title :block/name]` on `[:block/uuid uuid]`. + +Decide whether `fetch-uuid-entities` itself should move into the shared namespace or whether a small command-local wrapper should call a shared pure helper after performing the transport lookups. + +4. Update `src/main/logseq/cli/command/show.cljs` to consume the shared helpers. + +Keep `show` block label rendering behavior unchanged. + +Update `resolve-uuid-refs-in-node` and `resolve-uuid-refs-in-tree-data` to call the shared replacement helper rather than a private local implementation. + +5. Fix the `show` property rendering path. + +Update `property-value->string` and any surrounding helper signatures so plain string values, `:block/title` values coming from maps, and string `:logseq.property/value` values are also passed through the shared block-ref replacement helper before formatting. + +This is the special case that must not be missed, because a block property with default string content is user-visible text but is currently outside the main `block-label` replacement path. + +6. Update `src/main/logseq/cli/command/search.cljs` to normalize visible titles through the shared helper. + +Keep `search block` on `:thread-api/q`. + +Collect UUID refs from the raw `:block/title` values returned by search results. + +Fetch UUID labels through the same lookup strategy used by `show`. + +Replace block refs in the visible `:block/title` values before `normalize-items` strips helper-only data. + +Apply the same normalization rule to other search subcommands when they render `:block/title`, `:title`, or `:name` as user-visible title text and the underlying source can contain block refs. + +7. Update `src/main/logseq/cli/command/list.cljs` to normalize visible titles through the shared helper. + +Keep the existing `:thread-api/cli-list-*` worker calls and all current filtering, sorting, and pagination semantics. + +After raw items are returned and before `apply-fields`, collect UUID refs from every user-visible `:block/title` field that can be printed by the list formatters. + +Fetch UUID labels once per result set and rewrite the relevant title fields through the shared helper. + +8. Ensure the formatter layer does not become a second rendering source of truth. + +`src/main/logseq/cli/format.cljs` should keep treating provided `:block/title` values as already-rendered visible strings. + +Do not add formatter-only UUID replacement logic for list or search, because that would diverge from JSON and EDN output and from the `show` command path. + +9. Add and run targeted tests for `show` property rendering. + +Verify a plain string property value containing `[[uuid]]` renders as `[[label]]`. + +Verify a property map whose visible text comes from `:block/title` also renders through the same helper. + +Verify string `:logseq.property/value` rendering also uses the same helper when that string contains block refs. + +10. Add and run CLI e2e regressions. + +Add a `search block` case that reproduces the original bug and verifies visible label output. + +Add a `list` case that verifies a visible title containing a page ref is rendered with labels. + +Add a `show` case if the current e2e coverage model can express property output cleanly, otherwise make the unit tests exhaustive for the property-value scenarios. + +## Files to inspect and likely update + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/show.cljs` is the current source of truth for CLI-side UUID replacement and the main file for the show property-value fix. + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/search.cljs` is the primary search implementation file that should normalize visible title output while preserving the current query path. + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs` is the primary list implementation file that should normalize visible title output after worker data returns. + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/db_worker.cljs` should be inspected to confirm which list payload fields carry raw `:block/title` values today. + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs` should be inspected to verify that all list and search `TITLE` columns are downstream consumers of already-rendered title strings. + +`/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/uuid_refs.cljs` is the recommended destination for extracted shared block-ref rendering helpers. + +`/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/search_test.cljs` should hold the search command regressions. + +`/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs` should hold the visible table regressions for list and search. + +A list-command test namespace under `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/` should hold command-level list regressions if one does not already exist. + +A show-command test namespace under `/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/` should hold the property-value rendering regressions if one does not already exist. + +`/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn` should hold the end-to-end CLI regressions. + +## Acceptance criteria + +All `list`, `search`, and `show` paths that render user-visible `:block/title` text use one shared block-ref replacement implementation. + +`logseq search block -c foo` shows `foo [[bar]]` rather than `foo [[UUID]]` for raw stored titles that contain serialized refs. + +The `list` command family does not print raw block UUID refs in visible `TITLE` output when the source title contains block refs. + +The `show` command keeps its current visible node-title behavior after helper extraction. + +The `show` command also resolves block refs inside property values when the visible property text is a plain string, a map `:block/title`, or a string `:logseq.property/value`. + +Existing search sorting and recycle filtering semantics remain unchanged. + +Existing list filtering, sorting, pagination, and worker API usage remain unchanged. + +Formatter output contracts for list and search tables remain intact. + +## Risks and mitigations + +The main design risk is extracting helpers incompletely and leaving `show` property rendering on a different path from `show` block-label rendering. + +Mitigate that risk by writing explicit property-string and property-map regressions before extracting helpers. + +A second risk is accidentally creating a formatter-only solution for list and search while `show` continues to normalize earlier. + +Mitigate that risk by normalizing in command execution or shared rendering helpers before data reaches `format.cljs`. + +A third risk is broadening the command payload shape or changing query semantics while trying to fix a rendering problem. + +Mitigate that risk by preserving `:thread-api/q` for search and `:thread-api/cli-list-*` for list and by limiting new transport work to UUID label lookups. + +A fourth risk is missing secondary visible strings that conceptually represent block titles, especially `show` property values stored as default strings. + +Mitigate that risk by explicitly auditing `property-value->string`, `normalize-property-values`, and `node-property-lines` and by adding direct regressions for each relevant branch. + +## Rollback plan + +If the change behaves incorrectly, revert the list and search normalization integrations and the show property-value updates while preserving the new tests that document the intended unified behavior. + +If helper extraction causes too much churn, keep the extraction commit separate from the command integration commits so the move can be reverted cleanly without losing the behavioral tests. + +Do not add fallback branches that silently print raw UUIDs for some command families and labels for others. + +## Testing Details + +The most important tests are behavior tests that observe final visible command output rather than helper implementation details. + +The shared-helper tests should lock the block-ref replacement rules themselves. + +The command tests should prove that list and search normalize visible titles before formatting and that show property rendering uses the same replacement behavior as show block labels. + +The formatter tests should only verify table layout and visible rendered text after normalization. + +The e2e tests should validate the reported search bug and at least one list and one show scenario that exercise the unified rendering path. + +## Implementation Details + +- Extract one shared CLI helper for block-ref text replacement from `show.cljs`. +- Keep `show`, `list`, and `search` on their current data-retrieval semantics. +- Reuse `show`'s UUID entity lookup strategy for list and search normalization. +- Normalize visible title text before handing it to formatter code. +- Keep `format.cljs` as a presentation layer, not as a second rendering engine. +- Audit every visible `:block/title` rendering site in list, search, and show. +- Treat `show` property values as first-class visible title-like strings when they contain block refs. +- Cover plain strings, map `:block/title`, and string `:logseq.property/value` in tests. +- Preserve existing sorting, filtering, pagination, and recycle-filter behavior. + +## Question + +Should the shared helper namespace own only the pure text replacement helpers, or should it also own the transport-backed UUID label lookup helper so list, search, and show all call the exact same lookup orchestration entrypoint. + +The preferred answer is to share the pure replacement helpers for sure and to share the lookup orchestration as well if that can be done without forcing awkward dependencies between command namespaces. + +--- diff --git a/src/main/logseq/cli/command/list.cljs b/src/main/logseq/cli/command/list.cljs index 03e5850348..db0eca0f1f 100644 --- a/src/main/logseq/cli/command/list.cljs +++ b/src/main/logseq/cli/command/list.cljs @@ -6,6 +6,7 @@ [logseq.cli.command.task-status :as task-status-command] [logseq.cli.server :as cli-server] [logseq.cli.transport :as transport] + [logseq.cli.uuid-refs :as uuid-refs] [logseq.common.util :as common-util] [logseq.db.frontend.property :as db-property] [promesa.core :as p])) @@ -309,6 +310,12 @@ (some? offset) (->> (drop offset) vec) (some? limit) (->> (take limit) vec))) +(defn- normalize-visible-title-fields + [config repo items fields] + (let [uuid-strings (uuid-refs/collect-uuid-refs-from-items items fields)] + (p/let [uuid->label (uuid-refs/fetch-uuid-labels config repo uuid-strings)] + (uuid-refs/normalize-item-string-fields items fields uuid->label)))) + (defn- prepare-tag-item [item {:keys [with-properties with-extends fields]}] (cond-> item @@ -334,7 +341,8 @@ fields (parse-field-list (:fields options)) sorted (apply-sort items sort-field order list-page-field-map) limited (apply-offset-limit sorted (:offset options) (:limit options)) - final (apply-fields limited fields list-page-field-map)] + final (apply-fields limited fields list-page-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title])] {:status :ok :data {:items final}}))) @@ -352,7 +360,8 @@ prepared (mapv #(prepare-tag-item % options) items) sorted (apply-sort prepared sort-field order list-tag-field-map) limited (apply-offset-limit sorted (:offset options) (:limit options)) - final (apply-fields limited fields list-tag-field-map)] + final (apply-fields limited fields list-tag-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title])] {:status :ok :data {:items final}}))) @@ -369,7 +378,8 @@ prepared (mapv #(prepare-property-item % options) items) sorted (apply-sort prepared sort-field order list-property-field-map) limited (apply-offset-limit sorted (:offset options) (:limit options)) - final (apply-fields limited fields list-property-field-map)] + final (apply-fields limited fields list-property-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title])] {:status :ok :data {:items final}}))) @@ -423,7 +433,8 @@ fields (parse-field-list (:fields options)) sorted (apply-sort items sort-field order list-node-field-map) limited (apply-offset-limit sorted (:offset options) (:limit options)) - final (apply-fields limited fields list-node-field-map)] + final (apply-fields limited fields list-node-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title :block/page-title])] {:status :ok :data {:items final}}))) @@ -452,7 +463,8 @@ fields (parse-field-list (:fields options)) sorted (apply-sort items sort-field order list-asset-field-map) limited (apply-offset-limit sorted (:offset options) (:limit options)) - final (apply-fields limited fields list-asset-field-map)] + final (apply-fields limited fields list-asset-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title])] {:status :ok :data {:items final}}))) @@ -489,6 +501,7 @@ fields (parse-field-list (:fields normalized-options)) sorted (apply-sort items sort-field order list-task-field-map) limited (apply-offset-limit sorted (:offset normalized-options) (:limit normalized-options)) - final (apply-fields limited fields list-task-field-map)] + final (apply-fields limited fields list-task-field-map) + final (normalize-visible-title-fields cfg (:repo action) final [:block/title])] {:status :ok :data {:items final}})))))) diff --git a/src/main/logseq/cli/command/search.cljs b/src/main/logseq/cli/command/search.cljs index 5e44e0a9fc..947a3e3ee4 100644 --- a/src/main/logseq/cli/command/search.cljs +++ b/src/main/logseq/cli/command/search.cljs @@ -4,6 +4,7 @@ [logseq.cli.command.core :as core] [logseq.cli.server :as cli-server] [logseq.cli.transport :as transport] + [logseq.cli.uuid-refs :as uuid-refs] [logseq.db :as ldb] [promesa.core :as p])) @@ -102,12 +103,19 @@ :db/id)) vec)) -(defn- normalize-items +(defn- select-items [items] (->> (or items []) (filter map?) (map #(select-keys % [:db/id :db/ident :block/title])) - sort-items)) + vec)) + +(defn- normalize-items + [config repo items] + (let [sorted (sort-items (select-items items)) + uuid-strings (uuid-refs/collect-uuid-refs-from-items sorted [:block/title])] + (p/let [uuid->label (uuid-refs/fetch-uuid-labels config repo uuid-strings)] + (uuid-refs/normalize-item-string-fields sorted [:block/title] uuid->label)))) (defn- execute-search [action config] @@ -121,9 +129,10 @@ ;; so the recycle filter only applies to page and block search. filtered (cond->> (or result []) (#{:search-page :search-block} (:type action)) - (remove ldb/recycled?))] + (remove ldb/recycled?)) + items (normalize-items cfg (:repo action) filtered)] {:status :ok - :data {:items (normalize-items filtered)}}))) + :data {:items items}}))) (defn execute-search-block [action config] diff --git a/src/main/logseq/cli/command/show.cljs b/src/main/logseq/cli/command/show.cljs index 61921a06d2..a55338b36d 100644 --- a/src/main/logseq/cli/command/show.cljs +++ b/src/main/logseq/cli/command/show.cljs @@ -11,6 +11,7 @@ [logseq.cli.server :as cli-server] [logseq.cli.style :as style] [logseq.cli.transport :as transport] + [logseq.cli.uuid-refs :as uuid-refs] [logseq.common.util :as common-util] [logseq.db :as ldb] [logseq.db.frontend.property :as db-property] @@ -237,36 +238,12 @@ attach-user-properties attach-user-properties-to-entity) -(def ^:private uuid-ref-pattern #"\[\[([0-9a-fA-F-]{36})\]\]") -(def ^:private uuid-ref-max-depth 10) - (defn- tag-label [tag] (or (:block/title tag) (:block/name tag) (some-> (:block/uuid tag) str))) -(defn- replace-uuid-refs-once - [value uuid->label] - (if (and (string? value) (seq uuid->label)) - (string/replace value uuid-ref-pattern - (fn [[_ id]] - (if-let [label (get uuid->label (string/lower-case id))] - (str "[[" label "]]") - (str "[[" id "]]")))) - value)) - -(defn- replace-uuid-refs - [value uuid->label] - (loop [current value - remaining uuid-ref-max-depth] - (if (or (not (string? current)) (zero? remaining) (empty? uuid->label)) - current - (let [next (replace-uuid-refs-once current uuid->label)] - (if (= next current) - current - (recur next (dec remaining))))))) - (defn- tags->suffix [tags] (let [labels (->> tags @@ -316,40 +293,46 @@ (.toISOString (js/Date. ms))) (defn- property-value->string - ([value] (property-value->string value nil)) - ([value labels] - (cond - (string? value) (nonblank-string value) - (number? value) (or (get labels value) (str value)) - (uuid? value) (or (get labels value) (str value)) - (lookup-ref? value) (let [uuid (second value)] - (or (get labels uuid) (str uuid))) - (boolean? value) (str value) - (keyword? value) (str value) - (map? value) (or (nonblank-string (:block/title value)) - (nonblank-string (:block/name value)) - (when-let [id (:db/id value)] - (get labels id)) - (when-let [uuid (:block/uuid value)] - (get labels uuid)) - (when-let [val (:logseq.property/value value)] - (if (string? val) - (nonblank-string val) - (str val))) - (pr-str value)) - (some? value) (str value) - :else nil))) + ([value] (property-value->string value nil nil)) + ([value labels] (property-value->string value labels nil)) + ([value labels uuid->label] + (let [render-visible (fn [text] + (some-> text + nonblank-string + (uuid-refs/replace-uuid-refs uuid->label)))] + (cond + (string? value) (render-visible value) + (number? value) (render-visible (or (get labels value) (str value))) + (uuid? value) (render-visible (or (get labels value) (str value))) + (lookup-ref? value) (let [uuid (second value)] + (render-visible (or (get labels uuid) (str uuid)))) + (boolean? value) (str value) + (keyword? value) (str value) + (map? value) (or (render-visible (:block/title value)) + (render-visible (:block/name value)) + (when-let [id (:db/id value)] + (render-visible (get labels id))) + (when-let [uuid (:block/uuid value)] + (render-visible (get labels uuid))) + (when-let [val (:logseq.property/value value)] + (if (string? val) + (render-visible val) + (str val))) + (pr-str value)) + (some? value) (str value) + :else nil)))) (defn- normalize-property-values - ([value] (normalize-property-values value nil)) - ([value labels] + ([value] (normalize-property-values value nil nil)) + ([value labels] (normalize-property-values value labels nil)) + ([value labels uuid->label] (let [values (cond (set? value) (seq value) (sequential? value) value (nil? value) nil :else [value]) rendered (->> values - (map #(property-value->string % labels)) + (map #(property-value->string % labels uuid->label)) (remove string/blank?) vec)] (if (set? value) @@ -357,11 +340,12 @@ rendered)))) (defn- node-user-property-entries - ([node] (node-user-property-entries node nil)) - ([node labels] + ([node] (node-user-property-entries node nil nil)) + ([node labels] (node-user-property-entries node labels nil)) + ([node labels uuid->label] (->> node (filter (fn [[k _]] (displayable-property-key? k))) - (map (fn [[k v]] [k (normalize-property-values v labels)])) + (map (fn [[k v]] [k (normalize-property-values v labels uuid->label)])) (remove (fn [[_ values]] (empty? values))) vec))) @@ -385,8 +369,8 @@ (map #(str item-indent "- " %) values))))))) (defn- node-property-lines - [node property-titles property-value-labels indent] - (let [property-entries (->> (node-user-property-entries node property-value-labels) + [node property-titles property-value-labels uuid->label indent] + (let [property-entries (->> (node-user-property-entries node property-value-labels uuid->label) sort-property-entries)] (->> property-entries (mapcat (fn [[property-key values]] @@ -418,7 +402,7 @@ text text (:block/name node) (:block/name node) (:block/uuid node) (some-> (:block/uuid node) str)) - base (replace-uuid-refs base uuid->label) + base (uuid-refs/replace-uuid-refs base uuid->label) tags-suffix (tags->suffix (:block/tags node))] (cond (and base tags-suffix) (str base " " tags-suffix) @@ -428,8 +412,8 @@ (defn- resolve-uuid-refs-in-node [node uuid->label] (cond-> node - (:block/title node) (update :block/title replace-uuid-refs uuid->label) - (:block/name node) (update :block/name replace-uuid-refs uuid->label) + (:block/title node) (update :block/title uuid-refs/replace-uuid-refs uuid->label) + (:block/name node) (update :block/name uuid-refs/replace-uuid-refs uuid->label) (:block/children node) (update :block/children (fn [children] (mapv #(resolve-uuid-refs-in-node % uuid->label) children))) (:block/page node) (update :block/page (fn [page] @@ -545,13 +529,6 @@ :property-value-labels property-value-labels}))) groups)))) -(defn- extract-uuid-refs - [value] - (->> (re-seq uuid-ref-pattern (or value "")) - (map second) - (filter common-util/uuid-string?) - distinct)) - (defn- collect-uuid-refs [{:keys [root]} linked-refs] (let [collect-nodes (fn collect-nodes [node] @@ -565,35 +542,10 @@ (mapcat (fn [node] (keep node [:block/title :block/name]))) (remove string/blank?))] (->> texts - (mapcat extract-uuid-refs) + (mapcat uuid-refs/extract-uuid-refs) distinct vec))) -(defn- uuid-entity-label - [entity] - (let [uuid-str (some-> (:block/uuid entity) str)] - (or (:block/title entity) - (:block/name entity) - uuid-str))) - -(defn- fetch-uuid-entities - [config repo uuid-strings] - (if (seq uuid-strings) - (p/let [blocks (p/all (map (fn [uuid-str] - (transport/invoke config :thread-api/pull false - [repo [:db/id :block/uuid :block/title :block/name] - [:block/uuid (uuid uuid-str)]])) - uuid-strings))] - (->> blocks - (remove nil?) - (map (fn [block] - (let [uuid-str (some-> (:block/uuid block) str)] - [(string/lower-case uuid-str) - {:id (:db/id block) - :label (uuid-entity-label block)}]))) - (into {}))) - (p/resolved {}))) - (defn- collect-user-property-keys [{:keys [root linked-references]}] (letfn [(collect-node [node] @@ -923,7 +875,7 @@ (str id-padding (style-glyph prefix))) append-property-lines (fn [node prefix] (let [indent (property-indent prefix) - prop-lines (node-property-lines node property-titles property-value-labels indent)] + prop-lines (node-property-lines node property-titles property-value-labels uuid->label indent)] (doseq [line prop-lines] (swap! lines conj line)))) walk (fn walk [node prefix] @@ -1031,7 +983,7 @@ (or linked-refs {:count 0 :blocks []}) {:count 0 :blocks []}) uuid-refs (collect-uuid-refs tree-data linked-refs*) - uuid->entity (fetch-uuid-entities config (:repo action) uuid-refs) + uuid->entity (uuid-refs/fetch-uuid-entities config (:repo action) uuid-refs) uuid->label (->> uuid->entity (keep (fn [[uuid-key {:keys [label]}]] (when (seq label) @@ -1119,9 +1071,27 @@ (assoc tree-data :breadcrumb-line breadcrumb-line) tree-data))) +(defn- attach-property-value-uuid-labels + [config action tree-data] + (let [property-value-labels (:property-value-labels tree-data) + nested-uuid-refs (uuid-refs/collect-uuid-refs-from-strings (vals property-value-labels))] + (if (seq nested-uuid-refs) + (p/let [nested-uuid->entity (uuid-refs/fetch-uuid-entities config (:repo action) nested-uuid-refs) + nested-uuid->label (->> nested-uuid->entity + (keep (fn [[uuid-key {:keys [label]}]] + (when (seq label) + [uuid-key label]))) + (into {}))] + (-> tree-data + (update :uuid->entity merge nested-uuid->entity) + (update :uuid->label merge nested-uuid->label) + (update :referenced-uuids #(vec (distinct (concat (or % []) nested-uuid-refs)))))) + (p/resolved tree-data)))) + (defn- render-tree-text-with-properties [config action tree-data] (p/let [tree-data (attach-property-titles config (:repo action) tree-data) + tree-data (attach-property-value-uuid-labels config action tree-data) tree-data (attach-breadcrumb-line config action tree-data)] (render-tree-text tree-data action))) diff --git a/src/main/logseq/cli/uuid_refs.cljs b/src/main/logseq/cli/uuid_refs.cljs new file mode 100644 index 0000000000..ba054fe747 --- /dev/null +++ b/src/main/logseq/cli/uuid_refs.cljs @@ -0,0 +1,104 @@ +(ns logseq.cli.uuid-refs + "Shared CLI helpers for rendering block refs stored as `[[uuid]]`." + (:require [clojure.string :as string] + [logseq.cli.transport :as transport] + [logseq.common.util :as common-util] + [promesa.core :as p])) + +(def ^:private uuid-ref-pattern #"\[\[([0-9a-fA-F-]{36})\]\]") +(def ^:private uuid-ref-max-depth 10) +(def ^:private uuid-lookup-selector [:db/id :block/uuid :block/title :block/name]) + +(defn extract-uuid-refs + [value] + (->> (re-seq uuid-ref-pattern (or value "")) + (map second) + (filter common-util/uuid-string?) + (map string/lower-case) + distinct)) + +(defn- replace-uuid-refs-once + [value uuid->label] + (if (and (string? value) (seq uuid->label)) + (string/replace value uuid-ref-pattern + (fn [[_ id]] + (if-let [label (get uuid->label (string/lower-case id))] + (str "[[" label "]]" ) + (str "[[" id "]]")))) + value)) + +(defn replace-uuid-refs + [value uuid->label] + (loop [current value + remaining uuid-ref-max-depth] + (if (or (not (string? current)) (zero? remaining) (empty? uuid->label)) + current + (let [next (replace-uuid-refs-once current uuid->label)] + (if (= next current) + current + (recur next (dec remaining))))))) + +(defn collect-uuid-refs-from-strings + [values] + (->> values + (filter string?) + (mapcat extract-uuid-refs) + distinct + vec)) + +(defn collect-uuid-refs-from-items + [items fields] + (->> items + (mapcat (fn [item] + (keep #(get item %) fields))) + collect-uuid-refs-from-strings)) + +(defn normalize-item-string-fields + [items fields uuid->label] + (mapv (fn [item] + (reduce (fn [item field] + (cond-> item + (string? (get item field)) + (update field replace-uuid-refs uuid->label))) + item + fields)) + items)) + +(defn uuid-entity-label + [entity] + (let [uuid-str (some-> (:block/uuid entity) str)] + (or (:block/title entity) + (:block/name entity) + uuid-str))) + +(defn fetch-uuid-entities + [config repo uuid-strings] + (let [uuid-strings (->> uuid-strings + (filter common-util/uuid-string?) + (map string/lower-case) + distinct + vec)] + (if (seq uuid-strings) + (p/let [blocks (p/all (map (fn [uuid-str] + (transport/invoke config :thread-api/pull false + [repo uuid-lookup-selector + [:block/uuid (uuid uuid-str)]])) + uuid-strings))] + (->> blocks + (remove nil?) + (map (fn [block] + (let [uuid-str (some-> (:block/uuid block) str string/lower-case)] + [uuid-str + {:id (:db/id block) + :label (uuid-entity-label block)}]))) + (into {}))) + (p/resolved {})))) + +(defn fetch-uuid-labels + [config repo uuid-strings] + (p/let [uuid->entity (fetch-uuid-entities config repo uuid-strings)] + (->> uuid->entity + (keep (fn [[uuid-key {:keys [label]}]] + (when (seq label) + [uuid-key label]))) + (into {})))) diff --git a/src/test/logseq/cli/command/list_test.cljs b/src/test/logseq/cli/command/list_test.cljs new file mode 100644 index 0000000000..445a15aecd --- /dev/null +++ b/src/test/logseq/cli/command/list_test.cljs @@ -0,0 +1,86 @@ +(ns logseq.cli.command.list-test + (:require [cljs.test :refer [async deftest is]] + [logseq.cli.command.list :as list-command] + [logseq.cli.server :as cli-server] + [logseq.cli.transport :as transport] + [promesa.core :as p])) + +(deftest test-execute-list-page-renders-block-ref-labels + (async done + (let [ref-uuid "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + calls* (atom [])] + (-> (p/with-redefs [cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}) + transport/invoke (fn [_ method _ args] + (swap! calls* conj {:method method :args args}) + (case method + :thread-api/cli-list-pages + [{:db/id 1 + :block/title (str "Project [[" ref-uuid "]]" )}] + + :thread-api/pull + {:db/id 42 + :block/uuid (uuid ref-uuid) + :block/title "Home"} + + nil))] + (p/let [result (list-command/execute-list-page + {:type :list-page + :repo "demo" + :options {}} + {})] + (is (= :ok (:status result))) + (is (= [{:db/id 1 + :block/title "Project [[Home]]"}] + (get-in result [:data :items]))) + (is (= [:thread-api/cli-list-pages :thread-api/pull] + (mapv :method @calls*))) + (is (= ["demo" + [:db/id :block/uuid :block/title :block/name] + [:block/uuid (uuid ref-uuid)]] + (-> @calls* second :args))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + +(deftest test-execute-list-node-renders-title-like-fields-through-block-ref-labels + (async done + (let [ref-uuid "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + calls* (atom [])] + (-> (p/with-redefs [cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}) + transport/invoke (fn [_ method _ args] + (swap! calls* conj {:method method :args args}) + (case method + :thread-api/cli-list-nodes + [{:db/id 9 + :block/title (str "Task [[" ref-uuid "]]" ) + :block/page-title (str "Page [[" ref-uuid "]]" ) + :node/type "block" + :block/page-id 2}] + + :thread-api/pull + {:db/id 100 + :block/uuid (uuid ref-uuid) + :block/title "Resolved page"} + + nil))] + (p/let [result (list-command/execute-list-node + {:type :list-node + :repo "demo" + :options {}} + {})] + (is (= :ok (:status result))) + (is (= [{:db/id 9 + :block/title "Task [[Resolved page]]" + :block/page-title "Page [[Resolved page]]" + :node/type "block" + :block/page-id 2}] + (get-in result [:data :items]))) + (is (= [:thread-api/cli-list-nodes :thread-api/pull] + (mapv :method @calls*))) + (is (= ["demo" + [:db/id :block/uuid :block/title :block/name] + [:block/uuid (uuid ref-uuid)]] + (-> @calls* second :args))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) diff --git a/src/test/logseq/cli/command/search_test.cljs b/src/test/logseq/cli/command/search_test.cljs index 68cc1f6de6..27cc066f25 100644 --- a/src/test/logseq/cli/command/search_test.cljs +++ b/src/test/logseq/cli/command/search_test.cljs @@ -84,6 +84,41 @@ (is false (str "unexpected error: " e)))) (p/finally done))))) +(deftest test-execute-search-block-renders-block-ref-labels + (async done + (let [ref-uuid "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + calls* (atom [])] + (-> (p/with-redefs [cli-server/ensure-server! (fn [_ _] {:base-url "http://example"}) + transport/invoke (fn [_ method _ args] + (swap! calls* conj {:method method :args args}) + (case method + :thread-api/q + [{:db/id 7 + :block/title (str "foo [[" ref-uuid "]]" )}] + + :thread-api/pull + {:db/id 99 + :block/uuid (uuid ref-uuid) + :block/title "bar"} + + nil))] + (p/let [result (search-command/execute-search-block + {:type :search-block :repo "demo" :query "foo"} + {})] + (is (= :ok (:status result))) + (is (= [{:db/id 7 + :block/title "foo [[bar]]"}] + (get-in result [:data :items]))) + (is (= [:thread-api/q :thread-api/pull] + (mapv :method @calls*))) + (is (= ["demo" + [:db/id :block/uuid :block/title :block/name] + [:block/uuid (uuid ref-uuid)]] + (-> @calls* second :args))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally done))))) + (deftest test-execute-search-block-skips-block-on-recycled-page ;; Recycled pages get :block/parent set to the Recycle page id and ;; :logseq.property/deleted-at stamped on themselves. The recursive diff --git a/src/test/logseq/cli/command/show_test.cljs b/src/test/logseq/cli/command/show_test.cljs index 0fe9bbc128..bfe6cd47a2 100644 --- a/src/test/logseq/cli/command/show_test.cljs +++ b/src/test/logseq/cli/command/show_test.cljs @@ -214,6 +214,57 @@ (p/resolved nil)))) +(deftest test-tree->text-renders-block-refs-inside-string-property-values + (let [ref-uuid "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + output (-> (show-command/tree->text + {:root {:db/id 1 + :block/title "Root" + :user.property/summary (str "Depends on [[" ref-uuid "]]" )} + :uuid->label {(string/lower-case ref-uuid) "Resolved ref"} + :property-titles {:user.property/summary "Summary"} + :property-value-labels {}}) + style/strip-ansi)] + (is (string/includes? output "Summary: Depends on [[Resolved ref]]")))) + +(deftest test-tree->text-renders-block-refs-inside-map-backed-property-values + (let [title-ref-uuid "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + value-ref-uuid "cccccccc-cccc-cccc-cccc-cccccccccccc" + output (-> (show-command/tree->text + {:root {:db/id 1 + :block/title "Root" + :user.property/title-ref {:block/title (str "Mapped [[" title-ref-uuid "]]" )} + :user.property/value-ref {:logseq.property/value (str "Value [[" value-ref-uuid "]]" )}} + :uuid->label {(string/lower-case title-ref-uuid) "Title ref" + (string/lower-case value-ref-uuid) "Value ref"} + :property-titles {:user.property/title-ref "Title ref" + :user.property/value-ref "Value ref"} + :property-value-labels {}}) + style/strip-ansi)] + (is (string/includes? output "Title ref: Mapped [[Title ref]]")) + (is (string/includes? output "Value ref: Value [[Value ref]]")))) + +(deftest test-tree->text-renders-block-refs-inside-many-ref-default-property-values + ;; Simulates a normal block with a user property: + ;; - property name: "Reproducible steps" + ;; - :logseq.property/type :default + ;; - :db/valueType :db.type/ref + ;; - :db/cardinality :db.cardinality/many + ;; where the property values are referenced blocks and one referenced + ;; block title contains a serialized block ref. + (let [nested-ref-uuid "dddddddd-dddd-dddd-dddd-dddddddddddd" + output (-> (show-command/tree->text + {:root {:db/id 1 + :block/title "Root" + :user.property/reproducible-steps [42 43]} + :uuid->label {(string/lower-case nested-ref-uuid) "Resolved nested step"} + :property-titles {:user.property/reproducible-steps "Reproducible steps"} + :property-value-labels {42 (str "Step [[" nested-ref-uuid "]]" ) + 43 "Verify output"}}) + style/strip-ansi)] + (is (string/includes? output "Reproducible steps:")) + (is (string/includes? output "- Step [[Resolved nested step]]")) + (is (string/includes? output "- Verify output")))) + (defn- contains-block-uuid? [value] (cond diff --git a/src/test/logseq/cli/uuid_refs_test.cljs b/src/test/logseq/cli/uuid_refs_test.cljs new file mode 100644 index 0000000000..51d5b1452a --- /dev/null +++ b/src/test/logseq/cli/uuid_refs_test.cljs @@ -0,0 +1,41 @@ +(ns logseq.cli.uuid-refs-test + (:require [cljs.test :refer [deftest is]] + [logseq.cli.uuid-refs :as uuid-refs])) + +(deftest test-extract-uuid-refs + (let [uuid-a "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" + uuid-b "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"] + (is (= ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"] + (uuid-refs/extract-uuid-refs + (str "One [[" uuid-a "]] and two [[" uuid-b "]] and one again [[" uuid-a "]]")))))) + +(deftest test-replace-uuid-refs + (let [uuid-a "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + uuid-b "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + missing-uuid "cccccccc-cccc-cccc-cccc-cccccccccccc"] + (is (= "alpha [[Ref A]] omega [[Ref B]]" + (uuid-refs/replace-uuid-refs + (str "alpha [[" uuid-a "]] omega [[" uuid-b "]]" ) + {uuid-a "Ref A" + uuid-b "Ref B"}))) + (is (= (str "[[Parent [[Child]]]] and [[" missing-uuid "]]" ) + (uuid-refs/replace-uuid-refs + (str "[[" uuid-a "]] and [[" missing-uuid "]]" ) + {uuid-a (str "Parent [[" uuid-b "]]" ) + uuid-b "Child"}))) + (is (= (str "missing [[" missing-uuid "]]" ) + (uuid-refs/replace-uuid-refs + (str "missing [[" missing-uuid "]]" ) + {}))))) + +(deftest test-collect-uuid-refs-from-strings + (let [uuid-a "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA" + uuid-b "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"] + (is (= ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"] + (uuid-refs/collect-uuid-refs-from-strings + [(str "Step [[" uuid-a "]]" ) + nil + "No refs here" + (str "Then [[" uuid-b "]] and again [[" uuid-a "]]" )])))))