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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- Fixed custom fields feature to support all field types (Text, Simple, User, Version, Build, etc.), not just Enum types (#638)
- Custom fields now use dynamic type discovery to query the project's field configuration
- Added proper fallback behavior when field type cannot be determined

## [0.22.0] - 2025-03-18

### Added
Expand Down
38 changes: 37 additions & 1 deletion docs/commands/issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,41 @@ Create new issues in YouTrack projects.
* ``-t, --type TEXT`` - Issue type (e.g., Bug, Feature, Task)
* ``-p, --priority TEXT`` - Issue priority (e.g., Critical, High, Medium, Low)
* ``-a, --assignee TEXT`` - Username of the assignee
* ``-cf, --custom-field TEXT`` - Custom field in format "FieldName=value" (repeatable)

**Example:**
**Custom Fields**

The ``--custom-field`` option supports all YouTrack custom field types:

* **Enum fields**: Single and multi-value enum fields (e.g., Priority, Status)
* **Text fields**: Free-form text fields
* **Simple fields**: Integer and float numeric fields
* **User fields**: Single and multi-user fields (use login names)
* **Version fields**: Version bundle fields
* **Build fields**: Build bundle fields
* **Date/DateTime fields**: Date and date-time fields (use Unix timestamps in milliseconds)
* **Period fields**: Time period fields

The CLI automatically detects the field type from the project configuration. If type discovery fails, it falls back to enum type as a safe default.

**Examples:**

.. code-block:: bash

yt issues create PROJ-1 "Fix login bug" -d "Users cannot login with special characters" -t Bug -p High -a john.doe

# Using custom fields
yt issues create PROJ-1 "New task" -cf "Team=Backend" -cf "Sprint=Sprint 1"

# Text field
yt issues create PROJ-1 "Task" -cf "Notes=Some implementation notes"

# Integer field
yt issues create PROJ-1 "Task" -cf "StoryPoints=5"

# User field (use login name)
yt issues create PROJ-1 "Task" -cf "Reviewer=john.doe"

List Issues
~~~~~~~~~~~

Expand Down Expand Up @@ -125,15 +153,23 @@ Update existing issues with new field values.
* ``-p, --priority TEXT`` - New issue priority
* ``-a, --assignee TEXT`` - New assignee username
* ``-t, --type TEXT`` - New issue type
* ``-cf, --custom-field TEXT`` - Custom field in format "FieldName=value" (repeatable)
* ``--show-details`` - Show current issue details instead of updating

**Custom Fields**

The ``--custom-field`` option is repeatable and supports all YouTrack custom field types, consistent with the create command. See the Create Issues section for field type details.

**Examples:**

.. code-block:: bash

# Update issue priority and assignee
yt issues update PROJ-123 -p Critical -a jane.smith

# Update with custom fields
yt issues update PROJ-123 -cf "Team=Frontend" -cf "StoryPoints=8"

# View current issue details
yt issues update PROJ-123 --show-details

Expand Down
122 changes: 122 additions & 0 deletions youtrack_cli/custom_field_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,3 +331,125 @@ def is_multi_value_field(field_type: str) -> bool:
ProjectCustomFieldTypes.MULTI_OWN_BUILD,
}
return field_type in multi_value_types

@staticmethod
def create_simple_field(name: str, value: Union[int, float, str]) -> Dict[str, Any]:
"""
Create a simple (integer/float) custom field.

Args:
name: The field name
value: The numeric value

Returns:
Dictionary representing the custom field
"""
# Convert to appropriate numeric type
try:
if isinstance(value, (int, float)):
numeric_value = value
else:
# Try int first, then float
numeric_value = int(value) if str(value).isdigit() else float(value)
except (ValueError, TypeError):
# Fallback to string representation if conversion fails
numeric_value = str(value)

return {
"$type": IssueCustomFieldTypes.INTEGER,
"name": name,
"value": numeric_value,
}

@staticmethod
def create_date_field(name: str, value: str) -> Dict[str, Any]:
"""
Create a date custom field.

Args:
name: The field name
value: The date value (Unix timestamp in milliseconds)

Returns:
Dictionary representing the custom field
"""
try:
timestamp = int(value)
except (ValueError, TypeError):
timestamp = int(value) # Will raise if invalid

return {
"$type": IssueCustomFieldTypes.DATE,
"name": name,
"value": timestamp,
}

@staticmethod
def create_single_version_field(name: str, value: str) -> Dict[str, Any]:
"""
Create a single version custom field.

Args:
name: The field name
value: The version name

Returns:
Dictionary representing the custom field
"""
return {
"$type": IssueCustomFieldTypes.SINGLE_VERSION,
"name": name,
"value": {"$type": CustomFieldValueTypes.VERSION_BUNDLE_ELEMENT, "name": value},
}

@staticmethod
def create_single_build_field(name: str, value: str) -> Dict[str, Any]:
"""
Create a single build custom field.

Args:
name: The field name
value: The build name

Returns:
Dictionary representing the custom field
"""
return {
"$type": IssueCustomFieldTypes.SINGLE_BUILD,
"name": name,
"value": {"$type": CustomFieldValueTypes.BUILD_BUNDLE_ELEMENT, "name": value},
}

@staticmethod
def create_field_by_type(field_info: Dict[str, Any], name: str, value: str) -> Dict[str, Any]:
"""
Create a custom field using discovered type information.

Args:
field_info: Field information from discover_custom_field
name: Field name
value: Field value (as string from CLI)

Returns:
Formatted custom field dictionary
"""
issue_field_type = field_info.get("issue_field_type")

# Map issue field types to creation methods
if issue_field_type == IssueCustomFieldTypes.TEXT:
return CustomFieldManager.create_text_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.INTEGER:
return CustomFieldManager.create_simple_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.SINGLE_USER:
return CustomFieldManager.create_single_user_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.SINGLE_VERSION:
return CustomFieldManager.create_single_version_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.SINGLE_BUILD:
return CustomFieldManager.create_single_build_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.SINGLE_ENUM:
return CustomFieldManager.create_single_enum_field(name, value)
elif issue_field_type == IssueCustomFieldTypes.STATE:
return CustomFieldManager.create_state_field(name, value)
else:
# Fallback to enum for unknown types
return CustomFieldManager.create_single_enum_field(name, value)
62 changes: 58 additions & 4 deletions youtrack_cli/services/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,37 @@ async def create_issue(
}
)

# Handle generic custom fields - default to enum type
# Handle generic custom fields with field type discovery
if custom_fields:
for field_name, field_value in custom_fields.items():
custom_fields_list.append(CustomFieldManager.create_single_enum_field(field_name, field_value))
try:
# Discover field type from project configuration
from .projects import ProjectService

project_service = ProjectService(self.auth_manager)
field_info_result = await project_service.discover_custom_field(project_id, field_name)

if field_info_result["status"] == "success":
# Use discovered field type
field_info = field_info_result["data"]
custom_fields_list.append(
CustomFieldManager.create_field_by_type(field_info, field_name, field_value)
)
else:
# Fallback to enum type with helpful error message
logger.warning(
f"Could not discover type for field '{field_name}', "
f"falling back to enum type. Error: {field_info_result.get('message')}"
)
custom_fields_list.append(
CustomFieldManager.create_single_enum_field(field_name, field_value)
)
except Exception as e:
# Fallback to enum type on any error
logger.warning(
f"Error discovering field type for '{field_name}': {str(e)}, falling back to enum type"
)
custom_fields_list.append(CustomFieldManager.create_single_enum_field(field_name, field_value))

# Add custom fields if any were specified
if custom_fields_list:
Expand Down Expand Up @@ -246,10 +273,37 @@ async def update_issue(
}
)

# Handle generic custom fields - default to enum type
# Handle generic custom fields with field type discovery
if custom_fields:
for field_name, field_value in custom_fields.items():
custom_fields_list.append(CustomFieldManager.create_single_enum_field(field_name, field_value))
try:
# Discover field type from project configuration
from .projects import ProjectService

project_service = ProjectService(self.auth_manager)
field_info_result = await project_service.discover_custom_field(project_id, field_name)

if field_info_result["status"] == "success":
# Use discovered field type
field_info = field_info_result["data"]
custom_fields_list.append(
CustomFieldManager.create_field_by_type(field_info, field_name, field_value)
)
else:
# Fallback to enum type with helpful error message
logger.warning(
f"Could not discover type for field '{field_name}', "
f"falling back to enum type. Error: {field_info_result.get('message')}"
)
custom_fields_list.append(
CustomFieldManager.create_single_enum_field(field_name, field_value)
)
except Exception as e:
# Fallback to enum type on any error
logger.warning(
f"Error discovering field type for '{field_name}': {str(e)}, falling back to enum type"
)
custom_fields_list.append(CustomFieldManager.create_single_enum_field(field_name, field_value))

# Add custom fields if any
if custom_fields_list:
Expand Down
Loading
Loading