Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions conduit/client/maniphest.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,22 @@ def edit_task(

return self._make_request("maniphest.edit", params)

def edit_comment(self, transaction_phid: str, content: str) -> Dict[str, Any]:
"""
Edit the content of an existing task comment.

Args:
transaction_phid: PHID of the comment transaction (PHID-XACT-TASK-xxx)
content: New comment content

Returns:
The edited comment's transaction PHID, object PHID and content
"""
return self._make_request(
"maniphest.comment.edit",
{"transactionPHID": transaction_phid, "content": content},
)

def get_task_transactions(self, task_id: int) -> Dict[str, Any]:
"""
Get transaction history for a task.
Expand Down
27 changes: 27 additions & 0 deletions conduit/client/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ def send_message(
return self._make_request("harbormaster.sendmessage", params)


class DashboardClient(BasePhabricatorClient):
"""
Client for Dashboard API operations.

Only "dashboard.panel.edit" exists upstream: dashboards themselves and the
panel layout are UI-only, and panels cannot be read back over Conduit.
"""

def edit_panel(
self, panel_phid: str, transactions: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Apply transactions to an existing dashboard panel.

Args:
panel_phid: PHID of the panel to edit
transactions: List of transaction objects

Returns:
The edited panel's object and applied transactions
"""
params = build_transaction_params(
transactions=transactions, object_identifier=panel_phid
)
return self._make_request("dashboard.panel.edit", params)


class PasteClient(BasePhabricatorClient):
"""
Client for Paste (Code Snippets) API operations.
Expand Down
115 changes: 37 additions & 78 deletions conduit/client/project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

from conduit.client.base import BasePhabricatorClient
from conduit.utils import build_search_params, build_transaction_params
Expand Down Expand Up @@ -70,21 +70,29 @@ def create_project(
return self.edit_project(transactions)

def search_columns(
self, constraints: Dict[str, Any] = None, limit: int = 100
self,
constraints: Dict[str, Any] = None,
limit: int = 100,
after: Optional[str] = None,
before: Optional[str] = None,
) -> Dict[str, Any]:
"""
Read information about workboard columns.

Args:
constraints: Search constraints
limit: Maximum number of results to return
after: Cursor for the next page
before: Cursor for the previous page

Returns:
Column information
"""
params = build_search_params(
constraints=constraints,
limit=limit,
after=after,
before=before,
)
return self._make_request("project.column.search", params)

Expand All @@ -101,87 +109,38 @@ def query_projects(self, constraints: Dict[str, Any] = None) -> Dict[str, Any]:
params = constraints or {}
return self._make_request("project.query", params)

def create_column(
self, project_phid: str, name: str, limit: int = None
) -> Dict[str, Any]:
"""
Create a new workboard column in a project.

Args:
project_phid: PHID of the project to create column in
name: Name of the column
limit: Column limit (optional)

Returns:
Created column data
"""
transactions = [
{"type": "name", "value": name},
{"type": "projectPHID", "value": project_phid},
]

if limit is not None:
transactions.append({"type": "limit", "value": str(limit)})

params = build_transaction_params(transactions=transactions)
return self._make_request("project.column.create", params)

def edit_column(
self, column_phid: str, transactions: List[Dict[str, Any]]
self,
column_phid: Optional[str] = None,
project_phid: Optional[str] = None,
name: Optional[str] = None,
hidden: Optional[bool] = None,
limit: Optional[int] = None,
sequence: Optional[int] = None,
) -> Dict[str, Any]:
"""
Edit an existing workboard column.
Create or edit a workboard column.

Args:
column_phid: PHID of the column to edit
transactions: List of transaction objects
column_phid: PHID of the column to edit. Omit to create a column.
project_phid: PHID of the board, required when creating.
name: New column name, required when creating.
hidden: Whether the column is hidden from the board.
limit: Point limit for the column, 0 to remove it.
sequence: Position of the column on the board.

Returns:
Updated column data
"""
params = build_transaction_params(
transactions=transactions, object_identifier=column_phid
)
return self._make_request("project.column.edit", params)

def delete_column(self, column_phid: str) -> Dict[str, Any]:
"""
Delete a workboard column.
The column's id, phid, name, hidden, sequence, pointLimit,
isDefault and proxyPHID.
"""
params = {
"columnPHID": column_phid,
"projectPHID": project_phid,
"name": name,
"hidden": hidden,
"limit": limit,
"sequence": sequence,
}
params = {k: v for k, v in params.items() if v is not None}

Args:
column_phid: PHID of the column to delete

Returns:
Deletion result
"""
params = {"objectIdentifier": column_phid}
return self._make_request("project.column.delete", params)

# Convenience methods for common column operations
def update_column_name(self, column_phid: str, new_name: str) -> Dict[str, Any]:
"""
Update the name of a workboard column.

Args:
column_phid: PHID of the column to update
new_name: New name for the column

Returns:
Updated column data
"""
transactions = [{"type": "name", "value": new_name}]
return self.edit_column(column_phid, transactions)

def update_column_limit(self, column_phid: str, limit: int) -> Dict[str, Any]:
"""
Update the task limit of a workboard column.

Args:
column_phid: PHID of the column to update
limit: New task limit for the column

Returns:
Updated column data
"""
transactions = [{"type": "limit", "value": str(limit)}]
return self.edit_column(column_phid, transactions)
return self._make_request("project.column.edit", params)
21 changes: 21 additions & 0 deletions conduit/client/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from unittest import TestCase
from unittest.mock import patch

from conduit.client.maniphest import ManiphestClient
from conduit.utils import flatten_params


Expand Down Expand Up @@ -27,3 +29,22 @@ def test_flatten_params(self):
("test[1][a]", 4),
],
)


class TestManiphestEditComment(TestCase):
@patch("conduit.client.base.BasePhabricatorClient._make_request")
def test_edit_comment(self, mock_request):
client = ManiphestClient(
api_url="http://test.example.com/api/", api_token="test_token"
)
mock_request.return_value = {"transactionPHID": "PHID-XACT-TASK-abc"}

client.edit_comment("PHID-XACT-TASK-abc", "updated text")

mock_request.assert_called_once_with(
"maniphest.comment.edit",
{
"transactionPHID": "PHID-XACT-TASK-abc",
"content": "updated text",
},
)
42 changes: 42 additions & 0 deletions conduit/client/tests/test_workboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ManiphestSearchConstraints,
ManiphestTaskTransactionStatus,
)
from conduit.main_tools import _build_task_transactions

pytestmark = pytest.mark.integration

Expand Down Expand Up @@ -254,6 +255,47 @@ def test_workboard_data_structures(self):
if "attachments" in task:
self.assertIn("columns", task["attachments"])

def test_edit_column_and_move_a_task_into_it(self):
"""Create, move into, hide and reorder a column through project.column.edit."""
project_phid = os.getenv("PHABRICATOR_TEST_WORKBOARD_PROJECT_PHID")
if not project_phid:
self.skipTest("PHABRICATOR_TEST_WORKBOARD_PROJECT_PHID is not set")
if "project.column.edit" not in self.project_client._make_request(
"conduit.query", {}
):
self.skipTest("project.column.edit is not available on this server")

column = self.project_client.edit_column(
project_phid=project_phid, name=f"{self.test_project_prefix}_column"
)

task = self.maniphest_client.create_task(
title=f"Column move task {time.time_ns()}"
)
self.maniphest_client.edit_task(
object_identifier=task["phid"],
transactions=_build_task_transactions(
projects_add=[project_phid], column_phid=column["phid"]
),
)
moved = self.maniphest_client.search_tasks(
constraints={"phids": [task["phid"]]}, attachments={"columns": True}
)["data"][0]
board = moved["attachments"]["columns"]["boards"][project_phid]
self.assertEqual([c["phid"] for c in board["columns"]], [column["phid"]])

edited = self.project_client.edit_column(
column_phid=column["phid"], hidden=True, sequence=0
)
self.assertTrue(edited["hidden"])
self.assertEqual(edited["sequence"], 0)

searched = self.project_client.search_columns(
constraints={"phids": [column["phid"]]}
)["data"][0]["fields"]
self.assertTrue(searched["isHidden"])
self.assertEqual(searched["sequence"], 0)

def _validate_column_structure(self, column):
"""Helper method to validate column object structure."""
# Basic column fields
Expand Down
8 changes: 8 additions & 0 deletions conduit/client/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,13 @@ class ManiphestTaskTransactionMFA(ManiphestTaskTransactionBase):
value: bool


class ManiphestTaskTransactionPoints(ManiphestTaskTransactionBase):
"""Set the story points of a task (null clears the value)."""

type: Literal["points"]
value: Optional[Union[int, float]]


class ManiphestTaskTransactionReference(ManiphestTaskTransactionBase):
"""Update the Reference custom field (custom.skroutz:reference)."""

Expand Down Expand Up @@ -691,6 +698,7 @@ class ManiphestTaskTransactionDueDate(ManiphestTaskTransactionBase):
ManiphestTaskTransactionSubtype,
ManiphestTaskTransactionComment,
ManiphestTaskTransactionMFA,
ManiphestTaskTransactionPoints,
ManiphestTaskTransactionReference,
ManiphestTaskTransactionDueDate,
]
Expand Down
5 changes: 5 additions & 0 deletions conduit/client/unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from conduit.client.passphrase import PassphraseClient
from conduit.client.misc import (
ConduitClient,
DashboardClient,
FlagClient,
HarbormasterClient,
MacroClient,
Expand Down Expand Up @@ -307,6 +308,7 @@ def __init__(
self.macro = MacroClient(api_url, api_token, self.http_client)
self.flag = FlagClient(api_url, api_token, self.http_client)
self.phid = PhidClient(api_url, api_token, self.http_client)
self.dashboard = DashboardClient(api_url, api_token, self.http_client)

@retry_request(max_retries=3, retry_delay=1.0, retry_backoff=2.0)
@cached_request(ttl=300)
Expand Down Expand Up @@ -455,6 +457,9 @@ def __init__(
self.phid = PhidClient(
api_url, api_token, self.http_client, oauth_token=oauth_token
)
self.dashboard = DashboardClient(
api_url, api_token, self.http_client, oauth_token=oauth_token
)

def get_stats(self) -> Dict[str, Any]:
"""Get client statistics if enhanced features are enabled."""
Expand Down
Loading