diff --git a/pyproject.toml b/pyproject.toml index bca4770..b28ce9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planecli" -version = "0.3.0" +version = "0.4.0" description = "CLI for Plane.so project management" readme = "README.md" license = "MIT" diff --git a/src/planecli/commands/work_items.py b/src/planecli/commands/work_items.py index 0b361b7..b23554a 100644 --- a/src/planecli/commands/work_items.py +++ b/src/planecli/commands/work_items.py @@ -220,6 +220,7 @@ async def list_( assignee: str | None = None, state: str | None = None, labels: str | None = None, + parent: str | None = None, sort: str = "created", limit: Annotated[int, Parameter(alias="-l")] = 50, json: bool = False, @@ -236,6 +237,9 @@ async def list_( Filter by state name (comma-separated). labels Filter by label name (comma-separated). + parent + Filter by parent work item identifier (ABC-123), UUID, or name + (name requires --project). sort Sort by: created (default), updated. limit @@ -355,6 +359,25 @@ async def _fetch_and_enrich(proj_dict: dict) -> list[dict]: ) ] + # Filter by parent work item + if parent: + if project: + project_id = projects_to_list[0]["id"] if projects_to_list else None + if project_id: + parent_data = await resolve_work_item_async( + parent, client, workspace, project_id + ) + else: + parent_data, _ = await resolve_work_item_across_projects_async( + parent, client, workspace + ) + else: + parent_data, _ = await resolve_work_item_across_projects_async( + parent, client, workspace + ) + parent_id = parent_data["id"] + data = [d for d in data if d.get("parent") == parent_id] + # Filter by labels (comma-separated, OR logic, substring match) if labels: label_tokens = [ln.strip().lower() for ln in labels.split(",") if ln.strip()] diff --git a/tests/test_commands/test_work_items.py b/tests/test_commands/test_work_items.py index 14342bb..ba7822f 100644 --- a/tests/test_commands/test_work_items.py +++ b/tests/test_commands/test_work_items.py @@ -28,6 +28,7 @@ def _make_work_item_dict( labels: list | None = None, created_at: str = "2026-02-10T12:00:00Z", updated_at: str = "2026-02-10T12:00:00Z", + parent: str | None = None, ) -> dict: """Return a work item as a dict (how cached_list_work_items returns them).""" return { @@ -38,6 +39,7 @@ def _make_work_item_dict( "assignees": assignees or [], "labels": labels or [], "priority": "medium", + "parent": parent, "created_at": created_at, "updated_at": updated_at, } @@ -643,6 +645,53 @@ async def test_list_filter_by_multiple_states( names = {d["name"] for d in data} assert names == {"Todo task", "Progress task"} + @patch("planecli.commands.work_items.output") + @patch("planecli.commands.work_items.resolve_work_item_across_projects_async") + @patch("planecli.commands.work_items.get_workspace", return_value="test-ws") + @patch("planecli.commands.work_items.get_client") + @patch("planecli.commands.work_items.create_client") + @patch("planecli.cache.cached_list_work_items", new_callable=AsyncMock) + @patch("planecli.cache.cached_list_members", new_callable=AsyncMock) + @patch("planecli.cache.cached_list_projects", new_callable=AsyncMock) + @patch("planecli.cache.cached_list_states", new_callable=AsyncMock) + @patch("planecli.cache.cached_list_labels", new_callable=AsyncMock) + async def test_list_filter_by_parent( + self, + mock_cached_labels, + mock_cached_states, + mock_cached_projects, + mock_cached_members, + mock_cached_work_items, + mock_create_client, + mock_get_client, + mock_get_ws, + mock_resolve_parent, + mock_output, + ): + """--parent ABC-1 returns only items whose parent matches the resolved UUID.""" + mock_get_client.return_value = MagicMock() + mock_create_client.return_value = MagicMock() + mock_cached_members.return_value = [] + mock_cached_projects.return_value = [ + _make_project_dict("proj-1", "FE", "Frontend"), + ] + mock_cached_work_items.return_value = [ + _make_work_item_dict("wi-1", "Child A", 1, parent="parent-uuid"), + _make_work_item_dict("wi-2", "Child B", 2, parent="parent-uuid"), + _make_work_item_dict("wi-3", "Unrelated", 3, parent="other-uuid"), + _make_work_item_dict("wi-4", "Orphan", 4, parent=None), + ] + mock_cached_states.return_value = [] + mock_cached_labels.return_value = [] + mock_resolve_parent.return_value = ({"id": "parent-uuid"}, "proj-1") + + await list_(parent="FE-99") + + mock_resolve_parent.assert_called_once() + data = mock_output.call_args[0][0] + names = {d["name"] for d in data} + assert names == {"Child A", "Child B"} + @patch("planecli.commands.work_items.output") @patch("planecli.commands.work_items.get_workspace", return_value="test-ws") @patch("planecli.commands.work_items.get_client") diff --git a/uv.lock b/uv.lock index c501dd2..b79b274 100644 --- a/uv.lock +++ b/uv.lock @@ -243,7 +243,7 @@ wheels = [ [[package]] name = "planecli" -version = "0.2.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "cashews", extra = ["diskcache"] },