mirror of
https://github.com/logseq/logseq.git
synced 2026-05-19 02:12:41 +00:00
feat(cli): add task related cmds
This commit is contained in:
@@ -148,6 +148,22 @@
|
||||
:cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]
|
||||
:tags [:upsert :list :smoke]}
|
||||
|
||||
{:id "task-upsert-and-list-json"
|
||||
:setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"]
|
||||
:cmds ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert task --graph {{graph-arg}} --page TaskHome --status todo --priority high >/dev/null"
|
||||
"{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json list task --graph {{graph-arg}} --status todo --priority high --content task --fields id,title,status,priority --sort updated-at --order desc --limit 1"]
|
||||
:expect {:exit 0
|
||||
:stdout-json-paths {[:status] "ok"
|
||||
[:data :items 0 :block/title] "TaskHome"
|
||||
[:data :items 0 :logseq.property/status] "logseq.property/status.todo"
|
||||
[:data :items 0 :logseq.property/priority] "logseq.property/priority.high"}}
|
||||
:covers {:commands ["upsert task" "list task"]
|
||||
:options {:global ["--config" "--graph" "--data-dir" "--output"]
|
||||
:upsert ["--page" "--status" "--priority"]
|
||||
:list ["--status" "--priority" "--content" "--fields" "--limit" "--sort" "--order"]}}
|
||||
:cleanup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json server stop --graph {{graph-arg}}"]
|
||||
:tags [:upsert :list :smoke]}
|
||||
|
||||
{:id "block-upsert-and-show-json"
|
||||
:setup ["{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json graph create --graph {{graph-arg}} >/dev/null"
|
||||
"{{cli}} --data-dir {{data-dir-arg}} --config {{config-path-arg}} --output json upsert page --graph {{graph-arg}} --page Home >/dev/null"]
|
||||
|
||||
@@ -33,22 +33,27 @@
|
||||
:list
|
||||
{:commands ["list page"
|
||||
"list tag"
|
||||
"list property"]
|
||||
"list property"
|
||||
"list task"]
|
||||
:options ["--expand"
|
||||
"--fields"
|
||||
"--limit"
|
||||
"--offset"
|
||||
"--sort"
|
||||
"--order"
|
||||
"--status"
|
||||
"--priority"
|
||||
"--content"
|
||||
"--journal-only"
|
||||
"--with-properties"
|
||||
"--with-extends"
|
||||
"--with-classes"
|
||||
"--with-type"]}
|
||||
"--with-type"]}
|
||||
|
||||
:upsert
|
||||
{:commands ["upsert block"
|
||||
"upsert page"
|
||||
"upsert task"
|
||||
"upsert tag"
|
||||
"upsert property"]
|
||||
:options ["--id"
|
||||
@@ -60,6 +65,7 @@
|
||||
"--content"
|
||||
"--blocks-file"
|
||||
"--status"
|
||||
"--priority"
|
||||
"--update-tags"
|
||||
"--update-properties"
|
||||
"--remove-tags"
|
||||
|
||||
195
docs/agent-guide/078-logseq-cli-task-subcommands.md
Normal file
195
docs/agent-guide/078-logseq-cli-task-subcommands.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Logseq CLI Task Subcommands Implementation Plan
|
||||
|
||||
Goal: Add task-oriented subcommands to the current `logseq-cli` command surface with `list task` and `upsert task`, where a task is any node tagged with `#Task` (`:logseq.class/Task`), including both blocks and pages.
|
||||
|
||||
Architecture: Reuse the existing `logseq-cli -> transport/invoke -> db-worker-node thread-api` flow, and keep command parsing/build/validation in CLI command namespaces with db read logic in db-worker helpers.
|
||||
|
||||
Architecture: Keep behavior aligned with current `list`/`upsert`/`remove` conventions, and avoid introducing a parallel command framework for tasks.
|
||||
|
||||
Tech Stack: ClojureScript, `babashka.cli`, `promesa`, Datascript pull/query, existing `apply-outliner-ops` mutation path in db-worker.
|
||||
|
||||
Related: Builds on [069-logseq-cli-search-subcommands.md](/Users/rcmerci/gh-repos/logseq/docs/agent-guide/069-logseq-cli-search-subcommands.md), [071-logseq-cli-search-content-option.md](/Users/rcmerci/gh-repos/logseq/docs/agent-guide/071-logseq-cli-search-content-option.md), and [docs/cli/logseq-cli.md](/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md).
|
||||
|
||||
## Problem statement
|
||||
|
||||
Current CLI supports generic node operations (`list page/tag/property`, `upsert block/page/tag/property`, `remove block/page/tag/property`) but has no task-focused command path.
|
||||
|
||||
The product model already has built-in class `:logseq.class/Task` and task properties (`status`, `priority`, `deadline`, `scheduled`), so users currently need multiple generic commands or custom queries for common task workflows.
|
||||
|
||||
We need a first-class task command group that maps to existing implementation patterns and is script-friendly.
|
||||
|
||||
## Current baseline from implementation
|
||||
|
||||
`list` commands already expose pagination and sort contracts and depend on db-worker helper functions via `:thread-api/cli-list-*`.
|
||||
|
||||
`upsert block/page` already supports tag/property mutation via `--update-tags`, `--update-properties`, `--remove-tags`, and `--remove-properties`.
|
||||
|
||||
`remove` commands already implement robust selector validation, name/id disambiguation, and best-effort multi-id behavior for blocks.
|
||||
|
||||
Built-in class `:logseq.class/Task` exists and is treated as a tag class with task properties in db bootstrap.
|
||||
|
||||
## Command surface proposal (for discussion first)
|
||||
|
||||
This section is intentionally a proposal draft so we can converge on options before implementation.
|
||||
|
||||
### 1) `list task` options proposal
|
||||
|
||||
Recommended MVP options:
|
||||
|
||||
| Option | Type | Default | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `--status` | status alias | none | Accept same aliases as current `upsert block --status` normalization. |
|
||||
| `--priority` | `low|medium|high|urgent` | none | Task priority filter. |
|
||||
| `--content` | string | none | Case-insensitive substring filter on `:block/title`. |
|
||||
| `--fields` | csv | command default | Same pattern as existing `list` commands. |
|
||||
| `--limit` | long | none | Same as existing `list`. |
|
||||
| `--offset` | long | none | Same as existing `list`. |
|
||||
| `--sort` | enum | `updated-at` | Proposed sort fields: `updated-at`, `created-at`, `title`, `status`, `priority`. |
|
||||
| `--order` | `asc|desc` | `asc` | Same as existing `list`. |
|
||||
|
||||
Recommended default output fields:
|
||||
|
||||
`id,title,status,priority,scheduled,deadline,updated-at,created-at`.
|
||||
|
||||
Phase 2 optional filters:
|
||||
|
||||
`--scheduled-after`, `--scheduled-before`, `--deadline-after`, `--deadline-before`, and multi-status support.
|
||||
|
||||
### 2) `upsert task` options proposal
|
||||
|
||||
Recommended MVP options:
|
||||
|
||||
| Option | Type | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `--id` | long | Update an existing node as task by db/id. |
|
||||
| `--uuid` | uuid | Update an existing node as task by UUID. |
|
||||
| `--page` | string | Upsert a page task by page name. |
|
||||
| `--content` | string | Create a block task with content. |
|
||||
| `--target-id` | long | Block create target selector. |
|
||||
| `--target-uuid` | uuid | Block create target selector. |
|
||||
| `--target-page` | string | Block create target selector. |
|
||||
| `--pos` | `first-child|last-child|sibling` | Block create position control. |
|
||||
| `--status` | status alias | Set `:logseq.property/status`. |
|
||||
| `--priority` | enum | Set `:logseq.property/priority.*`. |
|
||||
| `--update-properties` | edn map | Advanced property mutation. |
|
||||
| `--remove-properties` | edn vector | Advanced property removal. |
|
||||
| `--update-tags` | edn vector | Optional extra tags beyond `#Task`. |
|
||||
| `--remove-tags` | edn vector | Optional tag removal with guardrails for `#Task`. |
|
||||
|
||||
Recommended semantics:
|
||||
|
||||
`upsert task` always ensures `:logseq.class/Task` tag exists on the target node.
|
||||
|
||||
When selector is `--id` or `--uuid`, command runs in update mode and converts non-task node into task by adding `:logseq.class/Task`.
|
||||
|
||||
When selector is `--page`, command upserts page and ensures task tag plus task properties.
|
||||
|
||||
When selector is `--content` and no id/page selector is provided, command creates block task and supports current block targeting options.
|
||||
|
||||
Key validation proposal:
|
||||
|
||||
Only one of `--id`, `--uuid`, `--page` is allowed.
|
||||
|
||||
`--content` and `--page` cannot be combined.
|
||||
|
||||
`--target-*` and `--pos` are only valid for block-create path.
|
||||
|
||||
Task removal strategy:
|
||||
|
||||
Do not add `remove task` in this scope.
|
||||
|
||||
Use existing `remove block` and `remove page` commands for deletion.
|
||||
|
||||
## Proposed architecture and files
|
||||
|
||||
Keep `list task` in list command flow and add db-worker helper method to avoid large query logic in CLI command layer.
|
||||
|
||||
Implement `upsert task` by reusing existing upsert/tag/property helper functions and `apply-outliner-ops` patterns.
|
||||
|
||||
Primary files to touch:
|
||||
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/db_worker.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/db_worker.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs](/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs)
|
||||
- [/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md](/Users/rcmerci/gh-repos/logseq/docs/cli/logseq-cli.md)
|
||||
- [/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn](/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_inventory.edn)
|
||||
- [/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn](/Users/rcmerci/gh-repos/logseq/cli-e2e/spec/non_sync_cases.edn)
|
||||
|
||||
## Testing Plan
|
||||
|
||||
I will follow @test-driven-development and add all failing tests before implementation code changes.
|
||||
|
||||
I will add parser and validation tests in [commands_test.cljs](/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/commands_test.cljs) for new command paths and option conflicts.
|
||||
|
||||
I will add command execution tests for new task logic in [upsert_test.cljs](/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/command/upsert_test.cljs) and existing remove/list test files, or create focused task command test namespaces if coverage becomes too broad.
|
||||
|
||||
I will add db-worker behavior tests for task listing in [db_worker_test.cljs](/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/common/db_worker_test.cljs).
|
||||
|
||||
I will add output formatting tests in [format_test.cljs](/Users/rcmerci/gh-repos/logseq/src/test/logseq/cli/format_test.cljs) for human/json/edn task command outputs.
|
||||
|
||||
I will extend CLI e2e inventory and non-sync cases for `list task` and `upsert task`.
|
||||
|
||||
I will run focused tests first and then run `bb dev:lint-and-test`.
|
||||
|
||||
NOTE: I will write *all* tests before I add any implementation behavior.
|
||||
|
||||
## Step-by-step implementation plan
|
||||
|
||||
1. Add command entries for `list task` and `upsert task` with proposed option specs and examples.
|
||||
|
||||
2. Wire new command keywords into parse-time validation in [commands.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs).
|
||||
|
||||
3. Add build-action branches for new task commands and keep error codes aligned with existing style (`:missing-target`, `:invalid-options`, `:missing-page-name`).
|
||||
|
||||
4. Add execute branch wiring for new task command types in [commands.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/commands.cljs).
|
||||
|
||||
5. Implement `list task` db-worker helper query in [db_worker.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/common/db_worker.cljs) and expose it via new `:thread-api/cli-list-tasks` in [db_core.cljs](/Users/rcmerci/gh-repos/logseq/src/main/frontend/worker/db_core.cljs).
|
||||
|
||||
6. Implement `execute-list-task` in [list.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/list.cljs) with existing sort/fields/offset/limit helpers.
|
||||
|
||||
7. Implement `build-task-action` and `execute-upsert-task` in [upsert.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/command/upsert.cljs) by composing existing tag/property resolution utilities and task-specific normalization for status and priority.
|
||||
|
||||
8. Add `format` support for new command result shapes in [format.cljs](/Users/rcmerci/gh-repos/logseq/src/main/logseq/cli/format.cljs).
|
||||
|
||||
9. Update docs and CLI e2e coverage files once behavior is stable.
|
||||
|
||||
## Edge cases to cover explicitly
|
||||
|
||||
A target node can exist but not have `#Task`, and command should fail with explicit type-mismatch style error instead of silently mutating or deleting wrong entities.
|
||||
|
||||
Task pages and task blocks must both be supported for list and upsert.
|
||||
|
||||
`upsert task` status aliases should match current `upsert block --status` normalization so scripts can reuse existing values.
|
||||
|
||||
`--scheduled` and `--deadline` are explicitly out of MVP and should be handled later via dedicated options or via `--update-properties`.
|
||||
|
||||
`list task` sort behavior must stay stable with deterministic tie-breaker by `:db/id`, consistent with current list commands.
|
||||
|
||||
## Testing Details
|
||||
|
||||
I will verify parse/build/execute behavior at command layer and confirm db-worker helper output contracts without relying on ad hoc manual checks.
|
||||
|
||||
I will ensure tests assert behavior for both block task and page task paths, including conversion of non-task nodes during upsert when selector-based update is used.
|
||||
|
||||
I will include at least one e2e case that creates task via `upsert task` and lists it via `list task` in one flow.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Keep task identity canonical as `:block/tags` containing `:logseq.class/Task`.
|
||||
- Reuse existing normalization helpers for status and property parsing to minimize duplicate parsing logic.
|
||||
- Prefer extending existing command namespaces over introducing a second task-only command framework.
|
||||
- Reuse existing list formatting utilities with task-specific column mapping.
|
||||
- Keep error code naming aligned with current conventions to avoid inconsistent CLI UX.
|
||||
- Keep JSON namespaced key behavior unchanged by routing through existing formatter normalization.
|
||||
- Ensure command help examples include both block and page task use cases.
|
||||
- Keep cli-e2e inventory and docs in lockstep with final option names.
|
||||
- Defer non-MVP filters (`deadline`/`scheduled` ranges, multi-status) and `upsert task` dedicated `--scheduled/--deadline` options.
|
||||
|
||||
## Question
|
||||
|
||||
No open questions.
|
||||
|
||||
---
|
||||
@@ -213,6 +213,7 @@ Inspect and edit commands:
|
||||
- `list page [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list pages (defaults to `--sort updated-at`)
|
||||
- `list tag [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list tags (defaults to `--sort updated-at`)
|
||||
- `list property [--expand] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list properties (defaults to `--sort updated-at`; `TYPE` and `CARDINALITY` are included by default even without `--expand`; missing schema cardinality is treated as `one`)
|
||||
- `list task [--status <status>] [--priority <low|medium|high|urgent>] [--content <text>] [--fields <csv>] [--limit <n>] [--offset <n>] [--sort <field>] [--order asc|desc]` - list task nodes tagged with `#Task` (supports both pages and blocks; defaults to `--sort updated-at`)
|
||||
- `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
|
||||
@@ -220,6 +221,9 @@ Inspect and edit commands:
|
||||
- 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 task --content <text> [--target-page <name>|--target-id <id>|--target-uuid <uuid>] [--pos first-child|last-child|sibling] [--status <status>] [--priority <low|medium|high|urgent>] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - create a task block and ensure `#Task` is attached
|
||||
- `upsert task --page <name> [--status <status>] [--priority <low|medium|high|urgent>] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - create/update a task page and ensure `#Task` is attached
|
||||
- `upsert task --id <id>|--uuid <uuid> [--status <status>] [--priority <low|medium|high|urgent>] [--update-tags <edn-vector>] [--update-properties <edn-map>] [--remove-tags <edn-vector>] [--remove-properties <edn-vector>]` - update an existing node and ensure `#Task` is attached
|
||||
- `upsert tag --name <name>` - create or upsert a tag by name
|
||||
- `upsert tag --id <id> [--name <name>]` - validate a tag by id; when `--name` is provided, rename that tag id (no-op if normalized name is unchanged)
|
||||
- `upsert tag --id <id> --name <name>` conflicts: returns `tag-name-conflict` when target name is a non-tag page, and `tag-rename-conflict` when target name is another existing tag
|
||||
@@ -245,8 +249,10 @@ Subcommands:
|
||||
list page [options] List pages
|
||||
list tag [options] List tags
|
||||
list property [options] List properties
|
||||
list task [options] List tasks
|
||||
upsert block [options] Upsert block
|
||||
upsert page [options] Upsert page
|
||||
upsert task [options] Upsert task
|
||||
upsert tag [options] Upsert tag
|
||||
upsert property [options] Upsert property
|
||||
move [options] Move block
|
||||
@@ -305,7 +311,7 @@ JSON key migration (flat -> namespaced):
|
||||
| `data.items[].type` | `data.items[].logseq.property/type` |
|
||||
| `data.items[].cardinality` | `data.items[].db/cardinality` |
|
||||
| `data.root.children[]` | `data.root.block/children[]` |
|
||||
- `upsert page` and `upsert block` return entity ids in `data.result` for JSON/EDN output, and include ids in human output.
|
||||
- `upsert page`, `upsert block`, and `upsert task` return entity ids in `data.result` for JSON/EDN output, and include ids in human output.
|
||||
- Human example:
|
||||
```text
|
||||
Upserted page:
|
||||
|
||||
@@ -1098,6 +1098,11 @@
|
||||
(let [conn (worker-state/get-datascript-conn repo)]
|
||||
(cli-db-worker/list-pages @conn options)))
|
||||
|
||||
(def-thread-api :thread-api/cli-list-tasks
|
||||
[repo options]
|
||||
(let [conn (worker-state/get-datascript-conn repo)]
|
||||
(cli-db-worker/list-tasks @conn options)))
|
||||
|
||||
;; API server specific fns start with 'api-'
|
||||
(def-thread-api :thread-api/api-get-page-data
|
||||
[repo page-title]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns logseq.cli.command.list
|
||||
"List-related CLI commands."
|
||||
(:require [clojure.string :as string]
|
||||
[logseq.cli.command.add :as add-command]
|
||||
[logseq.cli.command.core :as core]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
@@ -24,6 +25,12 @@
|
||||
|
||||
(def ^:private default-sort-field "updated-at")
|
||||
|
||||
(def ^:private task-priority-aliases
|
||||
{"low" :logseq.property/priority.low
|
||||
"medium" :logseq.property/priority.medium
|
||||
"high" :logseq.property/priority.high
|
||||
"urgent" :logseq.property/priority.urgent})
|
||||
|
||||
(defn- effective-sort-field
|
||||
[options]
|
||||
(or (:sort options) default-sort-field))
|
||||
@@ -103,6 +110,30 @@
|
||||
:default true
|
||||
:coerce :boolean}}))
|
||||
|
||||
(def ^:private list-task-field-map
|
||||
{"id" :db/id
|
||||
"title" :block/title
|
||||
"status" :logseq.property/status
|
||||
"priority" :logseq.property/priority
|
||||
"scheduled" :logseq.property/scheduled
|
||||
"deadline" :logseq.property/deadline
|
||||
"updated-at" :block/updated-at
|
||||
"created-at" :block/created-at})
|
||||
|
||||
(def ^:private list-task-spec
|
||||
(merge-with
|
||||
merge
|
||||
list-common-spec
|
||||
{:status {:desc "Filter by task status"
|
||||
:validate #{"todo" "doing" "done" "now" "later" "wait" "waiting"
|
||||
"backlog" "canceled" "cancelled"
|
||||
"in-review" "in_review" "inreview" "in-progress"}}
|
||||
:priority {:desc "Filter by task priority"
|
||||
:validate #{"low" "medium" "high" "urgent"}}
|
||||
:content {:desc "Filter by task title content"}
|
||||
:sort {:validate (set (keys list-task-field-map))}
|
||||
:fields {:multiple-values (keys list-task-field-map)}}))
|
||||
|
||||
(def entries
|
||||
[(core/command-entry ["list" "page"] :list-page "List pages" list-page-spec
|
||||
{:examples ["logseq list page --graph my-graph"
|
||||
@@ -113,7 +144,10 @@
|
||||
"logseq list tag --graph my-graph --include-built-in --limit 20 --output json"]})
|
||||
(core/command-entry ["list" "property"] :list-property "List properties" list-property-spec
|
||||
{:examples ["logseq list property --graph my-graph --with-type"
|
||||
"logseq list property --graph my-graph --include-built-in --limit 20 --output json"]})])
|
||||
"logseq list property --graph my-graph --include-built-in --limit 20 --output json"]})
|
||||
(core/command-entry ["list" "task"] :list-task "List tasks" list-task-spec
|
||||
{:examples ["logseq list task --graph my-graph --status todo --priority high"
|
||||
"logseq list task --graph my-graph --content \"release\" --sort updated-at --order desc"]})])
|
||||
|
||||
(defn invalid-options?
|
||||
[opts]
|
||||
@@ -234,3 +268,29 @@
|
||||
final (apply-fields limited fields list-property-field-map)]
|
||||
{:status :ok
|
||||
:data {:items final}})))
|
||||
|
||||
(defn- normalize-priority
|
||||
[value]
|
||||
(let [text (some-> value string/trim string/lower-case)]
|
||||
(when (seq text)
|
||||
(get task-priority-aliases text))))
|
||||
|
||||
(defn execute-list-task
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
options (:options action)
|
||||
normalized-options (cond-> options
|
||||
(seq (some-> (:status options) string/trim))
|
||||
(assoc :status (add-command/normalize-status (:status options)))
|
||||
(seq (some-> (:priority options) string/trim))
|
||||
(assoc :priority (normalize-priority (:priority options))))
|
||||
items (transport/invoke cfg :thread-api/cli-list-tasks false
|
||||
[(:repo action) normalized-options])
|
||||
sort-field (effective-sort-field normalized-options)
|
||||
order (or (:order normalized-options) "asc")
|
||||
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)]
|
||||
{:status :ok
|
||||
:data {:items final}})))
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
:complete :file}
|
||||
:status {:desc "Set task status"
|
||||
:validate #{"todo" "doing" "done" "now" "later" "wait" "waiting"
|
||||
"backlog" "canceled" "cancelled" "in-review" "in-progress"}}
|
||||
"backlog" "canceled" "cancelled"
|
||||
"in-review" "in_review" "inreview" "in-progress"}}
|
||||
:update-tags {:desc "Tags to add/update (EDN vector)"}
|
||||
:update-properties {:desc "Properties to add/update (EDN map)"}
|
||||
:remove-tags {:desc "Tags to remove (EDN vector) [update only]"}
|
||||
@@ -50,6 +51,35 @@
|
||||
:remove-tags {:desc "Tags to remove (EDN vector) [update only]"}
|
||||
:remove-properties {:desc "Properties to remove (EDN vector) [update only]"}})
|
||||
|
||||
(def ^:private upsert-task-spec
|
||||
{:id {:desc "Target node db/id (forces update mode) [update only]"
|
||||
:coerce :long}
|
||||
:uuid {:desc "Target node UUID (forces update mode) [update only]"
|
||||
:validate {:pred (comp parse-uuid str)
|
||||
:ex-msg (constantly "Option uuid must be a valid UUID string")}}
|
||||
:page {:desc "Task page name"
|
||||
:complete :pages}
|
||||
:content {:alias :c
|
||||
:desc "Task block content (create mode)"}
|
||||
:target-id {:desc "Target block db/id [create only]"
|
||||
:coerce :long}
|
||||
:target-uuid {:desc "Target block UUID [create only]"
|
||||
:validate {:pred (comp parse-uuid str)
|
||||
:ex-msg (constantly "Option target-uuid must be a valid UUID string")}}
|
||||
:target-page {:desc "Target page name [create only]"
|
||||
:complete :pages}
|
||||
:pos {:desc "Position. Default: last-child"
|
||||
:validate #{"first-child" "last-child" "sibling"}}
|
||||
:status {:desc "Set task status"
|
||||
:validate #{"todo" "doing" "done" "now" "later" "wait" "waiting"
|
||||
"backlog" "canceled" "cancelled" "in-review" "in-progress"}}
|
||||
:priority {:desc "Set task priority"
|
||||
:validate #{"low" "medium" "high" "urgent"}}
|
||||
:update-tags {:desc "Tags to add/update (EDN vector)"}
|
||||
:update-properties {:desc "Properties to add/update (EDN map)"}
|
||||
:remove-tags {:desc "Tags to remove (EDN vector) [update only]"}
|
||||
:remove-properties {:desc "Properties to remove (EDN vector) [update only]"}})
|
||||
|
||||
(def ^:private upsert-tag-spec
|
||||
{:id {:desc "Target tag db/id (forces update mode)"
|
||||
:coerce :long}
|
||||
@@ -82,6 +112,10 @@
|
||||
(core/command-entry ["upsert" "page"] :upsert-page "Upsert page" upsert-page-spec
|
||||
{:examples ["logseq upsert page --graph my-graph --page Home --update-tags '[\"project\"]'"
|
||||
"logseq upsert page --graph my-graph --id 999 --update-properties '{:logseq.property/description \"Example\"}'"]})
|
||||
(core/command-entry ["upsert" "task"] :upsert-task "Upsert task" upsert-task-spec
|
||||
{:examples ["logseq upsert task --graph my-graph --content \"Ship release\" --target-page Home --status todo --priority high"
|
||||
"logseq upsert task --graph my-graph --page Weekly Plan --status doing"
|
||||
"logseq upsert task --graph my-graph --id 123 --status done"]})
|
||||
(core/command-entry ["upsert" "tag"] :upsert-tag "Upsert tag" upsert-tag-spec
|
||||
{:examples ["logseq upsert tag --graph my-graph --name project"
|
||||
"logseq upsert tag --graph my-graph --id 200 --name Project Renamed"]})
|
||||
@@ -113,6 +147,18 @@
|
||||
"db.cardinality/many" "many"
|
||||
v)))
|
||||
|
||||
(def ^:private priority-aliases
|
||||
{"low" :logseq.property/priority.low
|
||||
"medium" :logseq.property/priority.medium
|
||||
"high" :logseq.property/priority.high
|
||||
"urgent" :logseq.property/priority.urgent})
|
||||
|
||||
(defn- normalize-priority
|
||||
[value]
|
||||
(let [text (some-> value string/trim string/lower-case)]
|
||||
(when (seq text)
|
||||
(get priority-aliases text))))
|
||||
|
||||
(defn invalid-options?
|
||||
[command opts]
|
||||
(case command
|
||||
@@ -138,6 +184,42 @@
|
||||
(when (> (count selectors) 1)
|
||||
"only one of --id or --page is allowed"))
|
||||
|
||||
:upsert-task
|
||||
(let [id (:id opts)
|
||||
uuid (some-> (:uuid opts) string/trim)
|
||||
page (some-> (:page opts) string/trim)
|
||||
content (some-> (:content opts) string/trim)
|
||||
selectors (filter some? [id uuid page])
|
||||
target-selectors (filter some? [(:target-id opts)
|
||||
(:target-uuid opts)
|
||||
(some-> (:target-page opts) string/trim)])
|
||||
pos (some-> (:pos opts) string/trim string/lower-case)
|
||||
selector-mode? (or (some? id) (seq uuid) (seq page))]
|
||||
(cond
|
||||
(> (count selectors) 1)
|
||||
"only one of --id, --uuid, or --page is allowed"
|
||||
|
||||
(and (seq page) (seq content))
|
||||
"--content and --page are mutually exclusive"
|
||||
|
||||
(and (or (some? id) (seq uuid)) (seq content))
|
||||
"--content is only valid when creating a block task"
|
||||
|
||||
(> (count target-selectors) 1)
|
||||
"only one of --target-id, --target-uuid, or --target-page is allowed"
|
||||
|
||||
(and selector-mode? (or (seq target-selectors) (seq pos)))
|
||||
"--target-* and --pos are only valid when creating a block task with --content"
|
||||
|
||||
(and (seq pos) (empty? target-selectors))
|
||||
"--pos is only valid when a target option is provided"
|
||||
|
||||
(and (= pos "sibling") (seq (some-> (:target-page opts) string/trim)))
|
||||
"--pos sibling is only valid for block targets"
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
:upsert-tag
|
||||
(let [name-provided? (contains? opts :name)
|
||||
name (normalize-tag-name (:name opts))]
|
||||
@@ -250,6 +332,102 @@
|
||||
(some? id) (assoc :id id)
|
||||
(seq page) (assoc :page page))}))))
|
||||
|
||||
(defn build-task-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
{:ok? false
|
||||
:error {:code :missing-repo
|
||||
:message "repo is required for upsert"}}
|
||||
(let [id (:id options)
|
||||
uuid (some-> (:uuid options) string/trim)
|
||||
page (some-> (:page options) string/trim)
|
||||
content (some-> (:content options) string/trim)
|
||||
status-provided? (contains? options :status)
|
||||
status-text (some-> (:status options) string/trim)
|
||||
status (when (seq status-text)
|
||||
(add-command/normalize-status status-text))
|
||||
priority-provided? (contains? options :priority)
|
||||
priority-text (some-> (:priority options) string/trim)
|
||||
priority (when (seq priority-text)
|
||||
(normalize-priority priority-text))
|
||||
task-properties (cond-> {}
|
||||
status (assoc :logseq.property/status status)
|
||||
priority (assoc :logseq.property/priority priority))
|
||||
update-tags-result (add-command/parse-tags-option (:update-tags options))
|
||||
update-properties-result (add-command/parse-properties-option
|
||||
(:update-properties options)
|
||||
{:allow-non-built-in? true})
|
||||
remove-tags-result (add-command/parse-tags-vector-option (:remove-tags options))
|
||||
remove-properties-result (add-command/parse-properties-vector-option
|
||||
(:remove-properties options)
|
||||
{:allow-non-built-in? true})
|
||||
mode (cond
|
||||
(seq page) :page
|
||||
(or (some? id) (seq uuid)) :update
|
||||
:else :create)
|
||||
create-options (cond-> options
|
||||
(seq (:target-page options))
|
||||
(assoc :target-page-name (:target-page options))
|
||||
true
|
||||
(dissoc :target-page))
|
||||
create-result (when (= mode :create)
|
||||
(add-command/build-add-block-action create-options [] repo))
|
||||
invalid-message (invalid-options? :upsert-task options)]
|
||||
(cond
|
||||
(seq invalid-message)
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message invalid-message}}
|
||||
|
||||
(and status-provided? (not status))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (str "invalid status: " (:status options))}}
|
||||
|
||||
(and priority-provided? (not priority))
|
||||
{:ok? false
|
||||
:error {:code :invalid-options
|
||||
:message (str "invalid priority: " (:priority options))}}
|
||||
|
||||
(and (not (some? id)) (not (seq uuid)) (not (seq page)) (not (seq content)))
|
||||
{:ok? false
|
||||
:error {:code :missing-target
|
||||
:message "block or page is required"}}
|
||||
|
||||
(not (:ok? update-tags-result))
|
||||
update-tags-result
|
||||
|
||||
(not (:ok? update-properties-result))
|
||||
update-properties-result
|
||||
|
||||
(not (:ok? remove-tags-result))
|
||||
remove-tags-result
|
||||
|
||||
(not (:ok? remove-properties-result))
|
||||
remove-properties-result
|
||||
|
||||
(and (= mode :create) (not (:ok? create-result)))
|
||||
create-result
|
||||
|
||||
:else
|
||||
{:ok? true
|
||||
:action (cond-> (if (= mode :create)
|
||||
(-> (:action create-result)
|
||||
(assoc :type :upsert-task))
|
||||
{:type :upsert-task
|
||||
:repo repo
|
||||
:graph (core/repo->graph repo)})
|
||||
true (assoc :mode mode
|
||||
:update-tags (:value update-tags-result)
|
||||
:update-properties (merge (or (:value update-properties-result) {})
|
||||
task-properties)
|
||||
:remove-tags (:value remove-tags-result)
|
||||
:remove-properties (:value remove-properties-result))
|
||||
(some? id) (assoc :id id)
|
||||
(seq uuid) (assoc :uuid uuid)
|
||||
(seq page) (assoc :page page)
|
||||
(and (seq content) (not= mode :page)) (assoc :content content))}))))
|
||||
|
||||
(defn build-tag-action
|
||||
[options repo]
|
||||
(if-not (seq repo)
|
||||
@@ -497,6 +675,98 @@
|
||||
:else
|
||||
entity)))
|
||||
|
||||
(def ^:private task-selector
|
||||
[:db/id :block/uuid :block/name :block/title])
|
||||
|
||||
(def ^:private task-tag-ident
|
||||
:logseq.class/Task)
|
||||
|
||||
(defn- normalize-lookup-uuid
|
||||
[value]
|
||||
(cond
|
||||
(uuid? value) value
|
||||
(and (string? value) (common-util/uuid-string? (string/trim value)))
|
||||
(uuid (string/trim value))
|
||||
:else nil))
|
||||
|
||||
(defn- pull-entity-by-uuid
|
||||
[config repo selector uuid-value]
|
||||
(when-let [uuid* (normalize-lookup-uuid uuid-value)]
|
||||
(transport/invoke config :thread-api/pull false
|
||||
[repo selector [:block/uuid uuid*]])))
|
||||
|
||||
(defn- ensure-task-node!
|
||||
[config repo {:keys [id uuid]}]
|
||||
(p/let [entity (cond
|
||||
(some? id)
|
||||
(pull-entity-by-id config repo task-selector id)
|
||||
|
||||
(seq uuid)
|
||||
(pull-entity-by-uuid config repo task-selector uuid)
|
||||
|
||||
:else
|
||||
nil)]
|
||||
(if (:db/id entity)
|
||||
entity
|
||||
(throw (ex-info "node not found for selector"
|
||||
{:code upsert-id-not-found-code
|
||||
:id id
|
||||
:uuid uuid})))))
|
||||
|
||||
(defn- ensure-task-tag-id!
|
||||
[config repo]
|
||||
(p/let [entity (transport/invoke config :thread-api/pull false
|
||||
[repo [:db/id] [:db/ident task-tag-ident]])]
|
||||
(if-let [tag-id (:db/id entity)]
|
||||
tag-id
|
||||
(throw (ex-info "task tag not found"
|
||||
{:code :task-tag-not-found})))))
|
||||
|
||||
(defn- task-property-overrides
|
||||
[action]
|
||||
(cond-> {}
|
||||
(:status action) (assoc :logseq.property/status (:status action))
|
||||
(:priority action) (assoc :logseq.property/priority (:priority action))))
|
||||
|
||||
(declare append-tag-and-property-ops)
|
||||
|
||||
(defn- execute-upsert-task-ops!
|
||||
[action cfg block-ids]
|
||||
(if (seq block-ids)
|
||||
(p/let [task-tag-id (ensure-task-tag-id! cfg (:repo action))
|
||||
update-tags (add-command/resolve-tags cfg (:repo action) (:update-tags action))
|
||||
remove-tags (add-command/resolve-tags cfg (:repo action) (:remove-tags action))
|
||||
update-properties (add-command/resolve-properties cfg (:repo action) (:update-properties action)
|
||||
{:allow-non-built-in? true})
|
||||
update-properties (merge (or update-properties {})
|
||||
(task-property-overrides action))
|
||||
remove-properties (add-command/resolve-property-identifiers cfg (:repo action)
|
||||
(:remove-properties action)
|
||||
{:allow-non-built-in? true})
|
||||
_ (ensure-property-identifiers-exist! cfg (:repo action) (keys update-properties))
|
||||
_ (ensure-property-identifiers-exist! cfg (:repo action) remove-properties)
|
||||
update-tag-ids (->> update-tags
|
||||
(map :db/id)
|
||||
(remove nil?)
|
||||
(cons task-tag-id)
|
||||
distinct
|
||||
vec)
|
||||
remove-tag-ids (->> remove-tags (map :db/id) (remove nil?) distinct vec)
|
||||
_ (when (some #{task-tag-id} remove-tag-ids)
|
||||
(throw (ex-info "cannot remove #Task tag in upsert task"
|
||||
{:code :invalid-options
|
||||
:message "cannot remove #Task tag in upsert task"})))
|
||||
ops (append-tag-and-property-ops []
|
||||
block-ids
|
||||
{:update-tag-ids update-tag-ids
|
||||
:remove-tag-ids remove-tag-ids
|
||||
:update-properties update-properties
|
||||
:remove-properties remove-properties})]
|
||||
(when (seq ops)
|
||||
(transport/invoke cfg :thread-api/apply-outliner-ops false
|
||||
[(:repo action) ops {}])))
|
||||
(p/resolved nil)))
|
||||
|
||||
(defn- append-tag-and-property-ops
|
||||
[ops block-ids {:keys [update-tag-ids remove-tag-ids update-properties remove-properties]}]
|
||||
(cond-> ops
|
||||
@@ -593,6 +863,39 @@
|
||||
:error {:code (or (get-in (ex-data e) [:code]) :exception)
|
||||
:message (or (ex-message e) (str e))}}))))
|
||||
|
||||
(defn execute-upsert-task
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))]
|
||||
(case (:mode action)
|
||||
:create
|
||||
(p/let [result (add-command/execute-add-block (assoc action :type :add-block) config)
|
||||
created-ids (vec (or (get-in result [:data :result]) []))
|
||||
_ (execute-upsert-task-ops! action cfg created-ids)]
|
||||
{:status :ok
|
||||
:data {:result created-ids}})
|
||||
|
||||
:page
|
||||
(p/let [page (ensure-page-entity! cfg (:repo action) (:page action))
|
||||
page-id (:db/id page)
|
||||
_ (execute-upsert-task-ops! action cfg [page-id])]
|
||||
{:status :ok
|
||||
:data {:result [page-id]}})
|
||||
|
||||
:update
|
||||
(p/let [entity (ensure-task-node! cfg (:repo action) action)
|
||||
node-id (:db/id entity)
|
||||
_ (execute-upsert-task-ops! action cfg [node-id])]
|
||||
{:status :ok
|
||||
:data {:result [node-id]}})
|
||||
|
||||
{:status :error
|
||||
:error {:code :invalid-options
|
||||
:message "invalid upsert task mode"}}))
|
||||
(p/catch (fn [e]
|
||||
{:status :error
|
||||
:error {:code (or (get-in (ex-data e) [:code]) :exception)
|
||||
:message (or (ex-message e) (str e))}}))))
|
||||
|
||||
(defn execute-upsert-tag
|
||||
[action config]
|
||||
(-> (p/let [cfg (cli-server/ensure-server! config (:repo action))
|
||||
|
||||
@@ -239,6 +239,16 @@
|
||||
(not (seq (:page opts))))
|
||||
(missing-page-name-result summary)
|
||||
|
||||
(and (= command :upsert-task) (upsert-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (upsert-command/invalid-options? command opts))
|
||||
|
||||
(and (= command :upsert-task)
|
||||
(not (some? (:id opts)))
|
||||
(not (seq (some-> (:uuid opts) string/trim)))
|
||||
(not (seq (some-> (:page opts) string/trim)))
|
||||
(not (seq (some-> (:content opts) string/trim))))
|
||||
(missing-target-result summary)
|
||||
|
||||
(and (= command :upsert-tag) (upsert-command/invalid-options? command opts))
|
||||
(command-core/invalid-options-result summary (upsert-command/invalid-options? command opts))
|
||||
|
||||
@@ -284,7 +294,7 @@
|
||||
(not (seq (some-> (:content opts) str string/trim))))
|
||||
(assoc (missing-query-text-result summary) :command command)
|
||||
|
||||
(and (#{:list-page :list-tag :list-property} command)
|
||||
(and (#{:list-page :list-tag :list-property :list-task} command)
|
||||
(list-command/invalid-options? opts))
|
||||
(command-core/invalid-options-result summary (list-command/invalid-options? opts))
|
||||
|
||||
@@ -513,7 +523,7 @@
|
||||
(:server-list :server-status :server-start :server-stop :server-restart)
|
||||
(server-command/build-action command server-repo)
|
||||
|
||||
(:list-page :list-tag :list-property)
|
||||
(:list-page :list-tag :list-property :list-task)
|
||||
(list-command/build-action command options repo)
|
||||
|
||||
(:search-block :search-page :search-property :search-tag)
|
||||
@@ -525,6 +535,9 @@
|
||||
:upsert-page
|
||||
(upsert-command/build-page-action options repo)
|
||||
|
||||
:upsert-task
|
||||
(upsert-command/build-task-action options repo)
|
||||
|
||||
:upsert-tag
|
||||
(upsert-command/build-tag-action options repo)
|
||||
|
||||
@@ -598,12 +611,14 @@
|
||||
:list-page (list-command/execute-list-page action config)
|
||||
:list-tag (list-command/execute-list-tag action config)
|
||||
:list-property (list-command/execute-list-property action config)
|
||||
:list-task (list-command/execute-list-task action config)
|
||||
:search-block (search-command/execute-search-block action config)
|
||||
:search-page (search-command/execute-search-page action config)
|
||||
:search-property (search-command/execute-search-property action config)
|
||||
:search-tag (search-command/execute-search-tag action config)
|
||||
:upsert-block (upsert-command/execute-upsert-block action config)
|
||||
:upsert-page (upsert-command/execute-upsert-page action config)
|
||||
:upsert-task (upsert-command/execute-upsert-task action config)
|
||||
:upsert-tag (upsert-command/execute-upsert-tag action config)
|
||||
:upsert-property (upsert-command/execute-upsert-property action config)
|
||||
:remove-block (remove-command/execute-remove-block action config)
|
||||
@@ -640,6 +655,7 @@
|
||||
:schema :query :lookup :selector
|
||||
:source :target :update-tags :update-properties
|
||||
:remove-tags :remove-properties
|
||||
:status :priority
|
||||
:src :dst :backup-name
|
||||
:export-type :file :import-type :input
|
||||
:graph-id :email :config-key :config-value])))))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns logseq.cli.common.db-worker
|
||||
"Cli fns for use with db-worker"
|
||||
(:require [datascript.core :as d]
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.frontend.entity-util :as entity-util]
|
||||
[logseq.db.frontend.property :as db-property]))
|
||||
@@ -72,6 +73,41 @@
|
||||
(update :logseq.property/description db-property/property-value-content))
|
||||
(minimal-list-item e)))))))
|
||||
|
||||
(defn- ref->ident
|
||||
[value]
|
||||
(cond
|
||||
(keyword? value) value
|
||||
(map? value) (:db/ident value)
|
||||
:else nil))
|
||||
|
||||
(defn- minimal-task-item
|
||||
[e]
|
||||
(cond-> (minimal-list-item e)
|
||||
true (assoc :logseq.property/status (ref->ident (:logseq.property/status e))
|
||||
:logseq.property/priority (ref->ident (:logseq.property/priority e))
|
||||
:logseq.property/scheduled (:logseq.property/scheduled e)
|
||||
:logseq.property/deadline (:logseq.property/deadline e))))
|
||||
|
||||
(defn list-tasks
|
||||
"List task nodes (both pages and blocks) tagged with :logseq.class/Task."
|
||||
[db {:keys [status priority content]}]
|
||||
(let [status* (ref->ident status)
|
||||
priority* (ref->ident priority)
|
||||
content* (some-> content str string/lower-case)]
|
||||
(->> (d/datoms db :avet :block/tags :logseq.class/Task)
|
||||
(map #(d/entity db (:e %)))
|
||||
(remove (fn [e]
|
||||
(and status*
|
||||
(not= status* (ref->ident (:logseq.property/status e))))))
|
||||
(remove (fn [e]
|
||||
(and priority*
|
||||
(not= priority* (ref->ident (:logseq.property/priority e))))))
|
||||
(remove (fn [e]
|
||||
(and (seq content*)
|
||||
(not (string/includes? (string/lower-case (or (:block/title e) ""))
|
||||
content*)))))
|
||||
(map minimal-task-item))))
|
||||
|
||||
(defn- parse-time
|
||||
[value]
|
||||
(cond
|
||||
@@ -118,4 +154,3 @@
|
||||
(cond-> (:db/ident e) (assoc :db/ident (:db/ident e)))
|
||||
(update :block/uuid str))
|
||||
(minimal-list-item e)))))))
|
||||
|
||||
|
||||
@@ -271,6 +271,38 @@
|
||||
[items now-ms]
|
||||
(format-list-dynamic items now-ms list-property-columns))
|
||||
|
||||
(defn- format-task-choice
|
||||
[value prefix]
|
||||
(let [ident (cond
|
||||
(keyword? value) value
|
||||
(map? value) (:db/ident value)
|
||||
:else nil)]
|
||||
(cond
|
||||
ident (let [name' (name ident)]
|
||||
(if (string/starts-with? name' prefix)
|
||||
(subs name' (count prefix))
|
||||
name'))
|
||||
(string? value) value
|
||||
:else "-")))
|
||||
|
||||
(def ^:private list-task-columns
|
||||
[["ID" (fn [item _] (or (:db/id item) (:id item))) [:db/id :id]]
|
||||
["TITLE" (fn [item _] (or (:title item) (:block/title item) (:name item))) [:title :block/title :name]]
|
||||
["STATUS" (fn [item _] (format-task-choice (or (:status item) (:logseq.property/status item)) "status."))
|
||||
[:status :logseq.property/status] true]
|
||||
["PRIORITY" (fn [item _] (format-task-choice (or (:priority item) (:logseq.property/priority item)) "priority."))
|
||||
[:priority :logseq.property/priority] true]
|
||||
["SCHEDULED" (fn [item _] (or (:scheduled item) (:logseq.property/scheduled item) "-"))
|
||||
[:scheduled :logseq.property/scheduled] true]
|
||||
["DEADLINE" (fn [item _] (or (:deadline item) (:logseq.property/deadline item) "-"))
|
||||
[:deadline :logseq.property/deadline] true]
|
||||
["UPDATED-AT" (fn [item now-ms] (human-ago (or (:updated-at item) (:block/updated-at item)) now-ms)) [:updated-at :block/updated-at] true]
|
||||
["CREATED-AT" (fn [item now-ms] (human-ago (or (:created-at item) (:block/created-at item)) now-ms)) [:created-at :block/created-at] true]])
|
||||
|
||||
(defn- format-list-task
|
||||
[items now-ms]
|
||||
(format-list-dynamic items now-ms list-task-columns))
|
||||
|
||||
(defn- quote-posix-shell
|
||||
[value]
|
||||
(str "'" (string/replace (normalize-cell value) #"'" "'\"'\"'") "'"))
|
||||
@@ -642,6 +674,10 @@
|
||||
[_context ids]
|
||||
(str "Upserted page:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-upsert-task
|
||||
[_context ids]
|
||||
(str "Upserted task:\n" (pr-str (vec (or ids [])))))
|
||||
|
||||
(defn- format-upsert-tag
|
||||
[_context ids]
|
||||
(str "Upserted tag:\n" (pr-str (vec (or ids [])))))
|
||||
@@ -776,10 +812,12 @@
|
||||
:list-page (format-list-page (:items data) now-ms)
|
||||
:list-tag (format-list-tag (:items data) now-ms)
|
||||
:list-property (format-list-property (:items data) now-ms)
|
||||
:list-task (format-list-task (:items data) now-ms)
|
||||
(:search-block :search-page :search-property :search-tag)
|
||||
(format-list-page (:items data) now-ms)
|
||||
:upsert-block (format-upsert-block context (:result data))
|
||||
:upsert-page (format-upsert-page context (:result data))
|
||||
:upsert-task (format-upsert-task context (:result data))
|
||||
:upsert-tag (format-upsert-tag context (:result data))
|
||||
:upsert-property (format-upsert-property context (:result data))
|
||||
:remove-block (format-remove-block context)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns logseq.cli.command.upsert-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
(:require [cljs.test :refer [async deftest is testing]]
|
||||
[logseq.cli.command.add :as add-command]
|
||||
[logseq.cli.command.upsert :as upsert-command]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.transport :as transport]
|
||||
@@ -51,3 +52,76 @@
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-build-task-action-validation
|
||||
(testing "upsert task requires target selector or content/page"
|
||||
(let [result (upsert-command/build-task-action {} "logseq_db_demo")]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-target (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects page and content combination"
|
||||
(let [result (upsert-command/build-task-action {:page "Home" :content "Task"} "logseq_db_demo")]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task build create mode"
|
||||
(let [result (upsert-command/build-task-action {:content "Task from CLI"
|
||||
:status "todo"
|
||||
:priority "high"}
|
||||
"logseq_db_demo")]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-task (get-in result [:action :type])))
|
||||
(is (= :create (get-in result [:action :mode])))
|
||||
(is (= :logseq.property/status.todo (get-in result [:action :update-properties :logseq.property/status])))
|
||||
(is (= :logseq.property/priority.high (get-in result [:action :update-properties :logseq.property/priority]))))))
|
||||
|
||||
(deftest test-execute-upsert-task-page-applies-task-ops
|
||||
(async done
|
||||
(let [ops* (atom nil)
|
||||
action {:type :upsert-task
|
||||
:mode :page
|
||||
:repo "demo-repo"
|
||||
:graph "demo-graph"
|
||||
:page "TaskHome"
|
||||
:status :logseq.property/status.todo
|
||||
:priority :logseq.property/priority.high}]
|
||||
(-> (p/with-redefs [cli-server/ensure-server! (fn [config _repo]
|
||||
(p/resolved (assoc config :base-url "http://example")))
|
||||
add-command/resolve-tags (fn [_ _ _] (p/resolved nil))
|
||||
add-command/resolve-properties (fn [_ _ _ _] (p/resolved {}))
|
||||
add-command/resolve-property-identifiers (fn [_ _ _ _] (p/resolved []))
|
||||
transport/invoke (fn [_ method _ args]
|
||||
(case method
|
||||
:thread-api/pull
|
||||
(let [[_ selector lookup] args]
|
||||
(cond
|
||||
(= lookup [:block/name "taskhome"])
|
||||
(p/resolved {:db/id 42 :block/uuid (uuid "00000000-0000-0000-0000-000000000042")})
|
||||
|
||||
(= lookup [:db/ident :logseq.class/Task])
|
||||
(p/resolved {:db/id 900})
|
||||
|
||||
(and (vector? selector) (= selector [:db/id]))
|
||||
(p/resolved {:db/id 1})
|
||||
|
||||
:else
|
||||
(p/resolved {})))
|
||||
|
||||
:thread-api/apply-outliner-ops
|
||||
(let [[_ ops _] args]
|
||||
(reset! ops* ops)
|
||||
(p/resolved nil))
|
||||
|
||||
(throw (ex-info "unexpected invoke"
|
||||
{:method method
|
||||
:args args}))))]
|
||||
(p/let [result (upsert-command/execute-upsert-task action {})]
|
||||
(is (= :ok (:status result)))
|
||||
(is (= [42] (get-in result [:data :result])))
|
||||
(is (= [[:batch-set-property [[42] :block/tags 900 {}]]
|
||||
[:batch-set-property [[42] :logseq.property/status :logseq.property/status.todo {}]]
|
||||
[:batch-set-property [[42] :logseq.property/priority :logseq.property/priority.high {}]]]
|
||||
@ops*))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[logseq.db.frontend.rules :as rules]
|
||||
[logseq.cli.command.show :as show-command]
|
||||
[logseq.cli.command.sync :as sync-command]
|
||||
[logseq.cli.command.upsert :as upsert-command]
|
||||
[logseq.cli.commands :as commands]
|
||||
[logseq.cli.server :as cli-server]
|
||||
[logseq.cli.style :as style]
|
||||
@@ -89,10 +90,12 @@
|
||||
(is (contains-bold? summary "list page"))
|
||||
(is (contains-bold? summary "list tag"))
|
||||
(is (contains-bold? summary "list property"))
|
||||
(is (contains-bold? summary "list task"))
|
||||
(is (contains-bold? summary "upsert block"))
|
||||
(is (contains-bold? summary "upsert page"))
|
||||
(is (contains-bold? summary "upsert tag"))
|
||||
(is (contains-bold? summary "upsert property"))
|
||||
(is (contains-bold? summary "upsert task"))
|
||||
(is (contains-bold? summary "remove block"))
|
||||
(is (contains-bold? summary "remove page"))
|
||||
(is (contains-bold? summary "remove tag"))
|
||||
@@ -137,96 +140,55 @@
|
||||
(is (string/includes? plain-summary "Global options:"))
|
||||
(is (string/includes? plain-summary "Command options:")))))
|
||||
|
||||
(deftest test-parse-args-help-groups
|
||||
(testing "graph group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["graph"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "graph list"))
|
||||
(is (string/includes? plain-summary "graph create"))
|
||||
(is (string/includes? plain-summary "graph export"))
|
||||
(is (string/includes? plain-summary "graph import"))
|
||||
(is (contains-bold? summary "graph list"))
|
||||
(is (contains-bold? summary "graph create"))
|
||||
(is (contains-bold? summary "graph export"))
|
||||
(is (contains-bold? summary "graph import"))))
|
||||
(deftest test-parse-args-help-groups-primary
|
||||
(testing "graph/list/upsert/server groups show subcommands"
|
||||
(doseq [[group plain-entries bold-entries]
|
||||
[["graph"
|
||||
["graph list" "graph create" "graph export" "graph import"]
|
||||
["graph list" "graph create" "graph export" "graph import"]]
|
||||
["list"
|
||||
["list page" "list tag" "list property" "list task"]
|
||||
["list page" "list tag" "list property" "list task"]]
|
||||
["upsert"
|
||||
["upsert task" "upsert tag" "upsert property"]
|
||||
["upsert task" "upsert tag" "upsert property"]]
|
||||
["server"
|
||||
["server list" "server start"]
|
||||
["server list" "server start"]]]]
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args [group]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(doseq [entry plain-entries]
|
||||
(is (string/includes? plain-summary entry)))
|
||||
(doseq [entry bold-entries]
|
||||
(is (contains-bold? summary entry)))
|
||||
(when (= "list" group)
|
||||
(is (string/includes? plain-summary "Global options:"))
|
||||
(is (string/includes? plain-summary "Command options:")))))))
|
||||
|
||||
(testing "list group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["list"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "list page"))
|
||||
(is (string/includes? plain-summary "list tag"))
|
||||
(is (string/includes? plain-summary "list property"))
|
||||
(is (contains-bold? summary "list page"))
|
||||
(is (contains-bold? summary "list tag"))
|
||||
(is (contains-bold? summary "list property"))
|
||||
(is (string/includes? plain-summary "Global options:"))
|
||||
(is (string/includes? plain-summary "Command options:"))))
|
||||
|
||||
(testing "upsert group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["upsert"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "upsert tag"))
|
||||
(is (string/includes? plain-summary "upsert property"))
|
||||
(is (contains-bold? summary "upsert tag"))
|
||||
(is (contains-bold? summary "upsert property"))))
|
||||
|
||||
(testing "server group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["server"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "server list"))
|
||||
(is (string/includes? plain-summary "server start"))
|
||||
(is (contains-bold? summary "server list"))
|
||||
(is (contains-bold? summary "server start"))))
|
||||
|
||||
(testing "query group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["query"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "query list"))
|
||||
(is (string/includes? plain-summary "query"))
|
||||
(is (contains-bold? summary "query list"))
|
||||
(is (contains-bold? summary "query"))))
|
||||
|
||||
(testing "search group shows subcommands"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["search"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "search block"))
|
||||
(is (string/includes? plain-summary "search page"))
|
||||
(is (string/includes? plain-summary "search property"))
|
||||
(is (string/includes? plain-summary "search tag"))
|
||||
(is (contains-bold? summary "search block"))
|
||||
(is (contains-bold? summary "search page"))
|
||||
(is (contains-bold? summary "search property"))
|
||||
(is (contains-bold? summary "search tag"))))
|
||||
|
||||
(testing "example group shows selectors"
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args ["example"]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(is (string/includes? plain-summary "example upsert"))
|
||||
(is (string/includes? plain-summary "example upsert page"))
|
||||
(is (string/includes? plain-summary "example show"))
|
||||
(is (contains-bold? summary "example upsert"))
|
||||
(is (contains-bold? summary "example show"))))
|
||||
(deftest test-parse-args-help-groups-secondary
|
||||
(testing "query/search/example groups show subcommands"
|
||||
(doseq [[group plain-entries bold-entries]
|
||||
[["query"
|
||||
["query list" "query"]
|
||||
["query list" "query"]]
|
||||
["search"
|
||||
["search block" "search page" "search property" "search tag"]
|
||||
["search block" "search page" "search property" "search tag"]]
|
||||
["example"
|
||||
["example upsert" "example upsert page" "example show"]
|
||||
["example upsert" "example show"]]]]
|
||||
(let [result (binding [style/*color-enabled?* true]
|
||||
(commands/parse-args [group]))
|
||||
summary (:summary result)
|
||||
plain-summary (strip-ansi summary)]
|
||||
(is (true? (:help? result)))
|
||||
(doseq [entry plain-entries]
|
||||
(is (string/includes? plain-summary entry)))
|
||||
(doseq [entry bold-entries]
|
||||
(is (contains-bold? summary entry))))))
|
||||
|
||||
(testing "group help command list omits [options]"
|
||||
(let [summary (:summary (binding [style/*color-enabled?* true]
|
||||
@@ -446,10 +408,12 @@
|
||||
(is (string/includes? plain-summary "upsert page"))
|
||||
(is (string/includes? plain-summary "upsert tag"))
|
||||
(is (string/includes? plain-summary "upsert property"))
|
||||
(is (string/includes? plain-summary "upsert task"))
|
||||
(is (contains-bold? summary "upsert block"))
|
||||
(is (contains-bold? summary "upsert page"))
|
||||
(is (contains-bold? summary "upsert tag"))
|
||||
(is (contains-bold? summary "upsert property")))))
|
||||
(is (contains-bold? summary "upsert property"))
|
||||
(is (contains-bold? summary "upsert task")))))
|
||||
|
||||
(deftest test-parse-args-help-alignment
|
||||
(testing "graph group aligns subcommand columns"
|
||||
@@ -1107,7 +1071,28 @@
|
||||
(is (true? (get-in result [:options :with-classes])))
|
||||
(is (true? (get-in result [:options :with-type])))
|
||||
(is (= "cardinality" (get-in result [:options :sort])))
|
||||
(is (= "name,type,cardinality" (get-in result [:options :fields]))))))
|
||||
(is (= "name,type,cardinality" (get-in result [:options :fields])))))
|
||||
|
||||
(testing "list task parses"
|
||||
(let [result (commands/parse-args ["list" "task"
|
||||
"--status" "doing"
|
||||
"--priority" "high"
|
||||
"--content" "alpha"
|
||||
"--fields" "id,title,status,priority"
|
||||
"--limit" "10"
|
||||
"--offset" "2"
|
||||
"--sort" "priority"
|
||||
"--order" "desc"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :list-task (:command result)))
|
||||
(is (= "doing" (get-in result [:options :status])))
|
||||
(is (= "high" (get-in result [:options :priority])))
|
||||
(is (= "alpha" (get-in result [:options :content])))
|
||||
(is (= "id,title,status,priority" (get-in result [:options :fields])))
|
||||
(is (= 10 (get-in result [:options :limit])))
|
||||
(is (= 2 (get-in result [:options :offset])))
|
||||
(is (= "priority" (get-in result [:options :sort])))
|
||||
(is (= "desc" (get-in result [:options :order]))))))
|
||||
|
||||
(deftest test-search-subcommand-parse
|
||||
(testing "search block parses --content option"
|
||||
@@ -1185,6 +1170,16 @@
|
||||
|
||||
(testing "list property rejects invalid sort field"
|
||||
(let [result (commands/parse-args ["list" "property" "--sort" "wat"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "list task rejects invalid sort field"
|
||||
(let [result (commands/parse-args ["list" "task" "--sort" "wat"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "list task rejects invalid priority"
|
||||
(let [result (commands/parse-args ["list" "task" "--priority" "wat"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
@@ -1537,6 +1532,77 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :unknown-command (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-upsert-task-mode
|
||||
(testing "upsert task parses block create mode"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--content" "Ship CLI tasks"
|
||||
"--target-page" "Home"
|
||||
"--status" "todo"
|
||||
"--priority" "high"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-task (:command result)))
|
||||
(is (= "Ship CLI tasks" (get-in result [:options :content])))
|
||||
(is (= "Home" (get-in result [:options :target-page])))
|
||||
(is (= "todo" (get-in result [:options :status])))
|
||||
(is (= "high" (get-in result [:options :priority])))))
|
||||
|
||||
(testing "upsert task parses page mode"
|
||||
(let [result (commands/parse-args ["upsert" "task" "--page" "Weekly Plan"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-task (:command result)))
|
||||
(is (= "Weekly Plan" (get-in result [:options :page])))))
|
||||
|
||||
(testing "upsert task parses id update mode"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--id" "42"
|
||||
"--status" "done"
|
||||
"--priority" "medium"])]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-task (:command result)))
|
||||
(is (= 42 (get-in result [:options :id])))
|
||||
(is (= "done" (get-in result [:options :status])))
|
||||
(is (= "medium" (get-in result [:options :priority])))))
|
||||
|
||||
(testing "upsert task requires selector, page, or content"
|
||||
(let [result (commands/parse-args ["upsert" "task"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-target (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects selector conflicts"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--id" "42"
|
||||
"--uuid" "11111111-1111-1111-1111-111111111111"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects page and content combination"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--page" "Home"
|
||||
"--content" "Task block"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects id and content combination"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--id" "42"
|
||||
"--content" "Task block"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects target options in page mode"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--page" "Home"
|
||||
"--target-page" "Elsewhere"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code])))))
|
||||
|
||||
(testing "upsert task rejects invalid priority"
|
||||
(let [result (commands/parse-args ["upsert" "task"
|
||||
"--content" "Alpha"
|
||||
"--priority" "wat"])]
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :invalid-options (get-in result [:error :code]))))))
|
||||
|
||||
(deftest test-verb-subcommand-parse-update-target-page
|
||||
(testing "upsert block update mode parses with target page"
|
||||
(let [result (commands/parse-args ["upsert" "block" "--id" "1" "--target-page" "Home"])]
|
||||
@@ -2020,6 +2086,12 @@
|
||||
(is (false? (:ok? result)))
|
||||
(is (= :missing-repo (get-in result [:error :code])))))
|
||||
|
||||
(testing "list task builds action"
|
||||
(let [parsed {:ok? true :command :list-task :options {:status "todo"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :list-task (get-in result [:action :type])))))
|
||||
|
||||
(testing "search page builds action from --content option"
|
||||
(let [parsed {:ok? true :command :search-page :options {:content "project home"} :args []}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
@@ -2072,7 +2144,15 @@
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :update (get-in result [:action :mode])))
|
||||
(is (= 42 (get-in result [:action :id]))))))
|
||||
(is (= 42 (get-in result [:action :id])))))
|
||||
|
||||
(testing "upsert task builds action"
|
||||
(let [parsed {:ok? true
|
||||
:command :upsert-task
|
||||
:options {:content "Task from CLI" :status "todo"}}
|
||||
result (commands/build-action parsed {:graph "demo"})]
|
||||
(is (true? (:ok? result)))
|
||||
(is (= :upsert-task (get-in result [:action :type]))))))
|
||||
|
||||
(deftest test-build-action-upsert-tag-property
|
||||
|
||||
@@ -3013,6 +3093,30 @@
|
||||
(p/catch (fn [e] (is false (str "unexpected error: " e))))
|
||||
(p/finally done))))
|
||||
|
||||
(deftest test-execute-task-dispatch
|
||||
(async done
|
||||
(let [calls* (atom [])]
|
||||
(-> (p/with-redefs [cli-server/list-graphs (fn [_] ["demo"])
|
||||
list-command/execute-list-task (fn [action _]
|
||||
(swap! calls* conj action)
|
||||
(p/resolved {:status :ok
|
||||
:data {:items []}}))
|
||||
upsert-command/execute-upsert-task (fn [action _]
|
||||
(swap! calls* conj action)
|
||||
(p/resolved {:status :ok
|
||||
:data {:result [101]}}))]
|
||||
(p/let [list-result (commands/execute {:type :list-task :repo "logseq_db_demo"} {})
|
||||
upsert-result (commands/execute {:type :upsert-task :repo "logseq_db_demo"} {})]
|
||||
(is (= :ok (:status list-result)))
|
||||
(is (= :list-task (:command list-result)))
|
||||
(is (= :ok (:status upsert-result)))
|
||||
(is (= :upsert-task (:command upsert-result)))
|
||||
(is (= [:list-task :upsert-task]
|
||||
(mapv :type @calls*)))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest test-execute-requires-existing-graph
|
||||
(async done
|
||||
(-> (p/with-redefs [cli-server/list-graphs (fn [_] [])
|
||||
|
||||
@@ -166,3 +166,65 @@
|
||||
(is (= default-ids invalid-created-after-ids))
|
||||
(is (= default-ids invalid-updated-after-ids)))))
|
||||
|
||||
(deftest test-list-tasks-contract
|
||||
(let [db0 (create-test-db)
|
||||
visible-id (->> (d/q '[:find [?e ...]
|
||||
:in $ ?title
|
||||
:where [?e :block/title ?title]]
|
||||
db0 "Visible Page")
|
||||
first)
|
||||
late-id (->> (d/q '[:find [?e ...]
|
||||
:in $ ?title
|
||||
:where [?e :block/title ?title]]
|
||||
db0 "Late Page")
|
||||
first)
|
||||
db1 (d/db-with db0 [[:db/add visible-id :block/tags :logseq.class/Task]
|
||||
[:db/add visible-id :logseq.property/status :logseq.property/status.todo]
|
||||
[:db/add visible-id :logseq.property/priority :logseq.property/priority.high]
|
||||
[:db/add visible-id :logseq.property/scheduled "2026-02-10T08:00:00.000Z"]
|
||||
[:db/add visible-id :logseq.property/deadline "2026-02-12T18:00:00.000Z"]
|
||||
[:db/add late-id :block/tags :logseq.class/Task]
|
||||
[:db/add late-id :logseq.property/status :logseq.property/status.done]
|
||||
[:db/add late-id :logseq.property/priority :logseq.property/priority.low]
|
||||
[:db/add -1 :block/title "Task Block Alpha"]
|
||||
[:db/add -1 :block/page visible-id]
|
||||
[:db/add -1 :block/parent visible-id]
|
||||
[:db/add -1 :block/order "a"]
|
||||
[:db/add -1 :block/created-at 7000]
|
||||
[:db/add -1 :block/updated-at 7100]
|
||||
[:db/add -1 :block/tags :logseq.class/Task]
|
||||
[:db/add -1 :logseq.property/status :logseq.property/status.doing]
|
||||
[:db/add -1 :logseq.property/priority :logseq.property/priority.high]])
|
||||
task-block-id (->> (d/q '[:find [?e ...]
|
||||
:in $ ?title
|
||||
:where
|
||||
[?e :block/title ?title]
|
||||
[?e :block/page]]
|
||||
db1 "Task Block Alpha")
|
||||
first)
|
||||
all-ids (list-item-ids (cli-db-worker/list-tasks db1 {}))
|
||||
status-ids (list-item-ids (cli-db-worker/list-tasks db1 {:status :logseq.property/status.todo}))
|
||||
priority-ids (list-item-ids (cli-db-worker/list-tasks db1 {:priority :logseq.property/priority.high}))
|
||||
content-ids (list-item-ids (cli-db-worker/list-tasks db1 {:content "aLpHa"}))
|
||||
combined-ids (list-item-ids (cli-db-worker/list-tasks db1 {:status :logseq.property/status.doing
|
||||
:priority :logseq.property/priority.high
|
||||
:content "alpha"}))
|
||||
visible-task (->> (cli-db-worker/list-tasks db1 {})
|
||||
(filter #(= visible-id (:db/id %)))
|
||||
first)]
|
||||
(testing "task list includes task pages and task blocks"
|
||||
(is (contains? all-ids visible-id))
|
||||
(is (contains? all-ids late-id))
|
||||
(is (contains? all-ids task-block-id)))
|
||||
|
||||
(testing "task filters apply status, priority, and content constraints"
|
||||
(is (= #{visible-id} status-ids))
|
||||
(is (= #{visible-id task-block-id} priority-ids))
|
||||
(is (= #{task-block-id} content-ids))
|
||||
(is (= #{task-block-id} combined-ids)))
|
||||
|
||||
(testing "task rows include task property fields"
|
||||
(is (= :logseq.property/status.todo (:logseq.property/status visible-task)))
|
||||
(is (= :logseq.property/priority.high (:logseq.property/priority visible-task)))
|
||||
(is (= "2026-02-10T08:00:00.000Z" (:logseq.property/scheduled visible-task)))
|
||||
(is (= "2026-02-12T18:00:00.000Z" (:logseq.property/deadline visible-task))))))
|
||||
|
||||
@@ -246,6 +246,26 @@
|
||||
"Count: 2")
|
||||
result)))))
|
||||
|
||||
(deftest test-human-output-list-task
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :list-task
|
||||
:data {:items [{:db/id 12
|
||||
:block/title "Alpha task"
|
||||
:logseq.property/status :logseq.property/status.todo
|
||||
:logseq.property/priority :logseq.property/priority.high
|
||||
:logseq.property/scheduled "2026-02-10T08:00:00.000Z"
|
||||
:logseq.property/deadline "2026-02-12T18:00:00.000Z"
|
||||
:block/created-at 40000
|
||||
:block/updated-at 90000}]}}
|
||||
{:output-format nil
|
||||
:now-ms 100000})]
|
||||
(is (string/includes? result "STATUS"))
|
||||
(is (string/includes? result "PRIORITY"))
|
||||
(is (string/includes? result "SCHEDULED"))
|
||||
(is (string/includes? result "DEADLINE"))
|
||||
(is (string/includes? result "Alpha task"))
|
||||
(is (string/includes? result "Count: 1"))))
|
||||
|
||||
(deftest test-human-output-search
|
||||
(testing "search block renders the list table contract"
|
||||
(let [result (format/format-result {:status :ok
|
||||
@@ -319,102 +339,104 @@
|
||||
(is (= "logseq.class/Tag" (get-in parsed-json [:data :item :db/ident])))
|
||||
(is (= "2a847b91-1565-49cc-9f9f-0f6ee25ca0f3" (get-in parsed-json [:data :item :block/uuid])))))
|
||||
|
||||
(deftest test-human-output-add-upsert-remove
|
||||
(testing "upsert block renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-block
|
||||
:context {:repo "demo-repo"
|
||||
:blocks ["a" "b"]}
|
||||
:data {:result [201 202]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted blocks:\n[201 202]" result))))
|
||||
(deftest test-human-output-upsert-success-lines
|
||||
(doseq [[label payload expected]
|
||||
[["upsert block renders ids in two lines"
|
||||
{:status :ok
|
||||
:command :upsert-block
|
||||
:context {:repo "demo-repo"
|
||||
:blocks ["a" "b"]}
|
||||
:data {:result [201 202]}}
|
||||
"Upserted blocks:\n[201 202]"]
|
||||
["upsert page renders ids in two lines"
|
||||
{:status :ok
|
||||
:command :upsert-page
|
||||
:context {:repo "demo-repo"
|
||||
:page "Home"}
|
||||
:data {:result [123]}}
|
||||
"Upserted page:\n[123]"]
|
||||
["upsert tag renders ids in two lines"
|
||||
{:status :ok
|
||||
:command :upsert-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result [321]}}
|
||||
"Upserted tag:\n[321]"]
|
||||
["upsert property renders ids in two lines"
|
||||
{:status :ok
|
||||
:command :upsert-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result [654]}}
|
||||
"Upserted property:\n[654]"]
|
||||
["upsert task renders ids in two lines"
|
||||
{:status :ok
|
||||
:command :upsert-task
|
||||
:context {:repo "demo-repo"
|
||||
:page "Weekly Plan"}
|
||||
:data {:result [987]}}
|
||||
"Upserted task:\n[987]"]]]
|
||||
(testing label
|
||||
(let [result (format/format-result payload {:output-format nil})]
|
||||
(is (= expected result))))))
|
||||
|
||||
(testing "upsert page renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-page
|
||||
:context {:repo "demo-repo"
|
||||
:page "Home"}
|
||||
:data {:result [123]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted page:\n[123]" result))))
|
||||
(deftest test-human-output-remove-success-lines
|
||||
(doseq [[label payload expected]
|
||||
[["remove page renders a succinct success line"
|
||||
{:status :ok
|
||||
:command :remove-page
|
||||
:context {:repo "demo-repo"
|
||||
:page "Home"}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed page: Home (repo: demo-repo)"]
|
||||
["remove block with id list renders block count"
|
||||
{:status :ok
|
||||
:command :remove-block
|
||||
:context {:repo "demo-repo"
|
||||
:ids [1 2 3]}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed blocks: 3 (repo: demo-repo)"]
|
||||
["remove tag renders a succinct success line"
|
||||
{:status :ok
|
||||
:command :remove-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed tag: Quote (repo: demo-repo)"]
|
||||
["remove property renders a succinct success line"
|
||||
{:status :ok
|
||||
:command :remove-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result {:ok true}}}
|
||||
"Removed property: owner (repo: demo-repo)"]]]
|
||||
(testing label
|
||||
(let [result (format/format-result payload {:output-format nil})]
|
||||
(is (= expected result))))))
|
||||
|
||||
(testing "upsert tag renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result [321]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted tag:\n[321]" result))))
|
||||
|
||||
(testing "upsert property renders ids in two lines"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result [654]}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted property:\n[654]" result))))
|
||||
|
||||
(testing "remove page renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-page
|
||||
:context {:repo "demo-repo"
|
||||
:page "Home"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed page: Home (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove block with id list renders block count"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-block
|
||||
:context {:repo "demo-repo"
|
||||
:ids [1 2 3]}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed blocks: 3 (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove tag renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-tag
|
||||
:context {:repo "demo-repo"
|
||||
:name "Quote"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed tag: Quote (repo: demo-repo)" result))))
|
||||
|
||||
(testing "remove property renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :remove-property
|
||||
:context {:repo "demo-repo"
|
||||
:name "owner"}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Removed property: owner (repo: demo-repo)" result))))
|
||||
|
||||
(testing "upsert block update mode renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-block
|
||||
:context {:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
:target "target-uuid"
|
||||
:update-tags ["TagA"]
|
||||
:update-properties {:logseq.property/publishing-public? true}
|
||||
:remove-tags ["TagB"]
|
||||
:remove-properties [:logseq.property/deadline]}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)" result))))
|
||||
|
||||
(testing "upsert block update without move target renders a succinct success line"
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-block
|
||||
:context {:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
:update-tags ["TagA"]}
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= "Upserted block: source-uuid (repo: demo-repo, tags:+1)" result)))))
|
||||
(deftest test-human-output-upsert-block-update-summary
|
||||
(doseq [[label context expected]
|
||||
[["upsert block update mode renders a succinct success line"
|
||||
{:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
:target "target-uuid"
|
||||
:update-tags ["TagA"]
|
||||
:update-properties {:logseq.property/publishing-public? true}
|
||||
:remove-tags ["TagB"]
|
||||
:remove-properties [:logseq.property/deadline]}
|
||||
"Upserted block: source-uuid -> target-uuid (repo: demo-repo, tags:+1, properties:+1, remove-tags:+1, remove-properties:+1)"]
|
||||
["upsert block update without move target renders a succinct success line"
|
||||
{:repo "demo-repo"
|
||||
:source "source-uuid"
|
||||
:update-tags ["TagA"]}
|
||||
"Upserted block: source-uuid (repo: demo-repo, tags:+1)"]]]
|
||||
(testing label
|
||||
(let [result (format/format-result {:status :ok
|
||||
:command :upsert-block
|
||||
:context context
|
||||
:data {:result {:ok true}}}
|
||||
{:output-format nil})]
|
||||
(is (= expected result))))))
|
||||
|
||||
(deftest test-human-output-graph-import-export
|
||||
(testing "graph export renders a succinct success line"
|
||||
|
||||
Reference in New Issue
Block a user