diff --git a/src/YouTrackSharp/ConnectionExtensions.cs b/src/YouTrackSharp/ConnectionExtensions.cs
index 6958afeb..3d3b2f93 100644
--- a/src/YouTrackSharp/ConnectionExtensions.cs
+++ b/src/YouTrackSharp/ConnectionExtensions.cs
@@ -59,5 +59,15 @@ public static TimeTrackingManagementService CreateTimeTrackingManagementService(
{
return new TimeTrackingManagementService(connection);
}
+
+ ///
+ /// Creates a .
+ ///
+ /// The to create a service with.
+ /// for accessing custom project fields.
+ public static ProjectCustomFieldsService ProjectCustomFieldsService(this Connection connection)
+ {
+ return new ProjectCustomFieldsService(connection);
+ }
}
}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Projects/CustomField.cs b/src/YouTrackSharp/Projects/CustomField.cs
new file mode 100644
index 00000000..94aec834
--- /dev/null
+++ b/src/YouTrackSharp/Projects/CustomField.cs
@@ -0,0 +1,52 @@
+using Newtonsoft.Json;
+
+namespace YouTrackSharp.Projects
+{
+ ///
+ /// Custom field for a project
+ ///
+ public class CustomField
+ {
+ ///
+ /// Creates an instance of the class.
+ ///
+ public CustomField()
+ {
+ Name = string.Empty;
+ Url = string.Empty;
+ Type = string.Empty;
+ CanBeEmpty = false;
+ EmptyText = string.Empty;
+ }
+
+ ///
+ /// Name of project custom field.
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// The Url of the custom field.
+ ///
+ [JsonProperty("url")]
+ public string Url { get; set; }
+
+ ///
+ /// Type of this custom field.
+ ///
+ [JsonProperty("type")]
+ public string Type { get; set; }
+
+ ///
+ /// Mandatory binary parameter defining if the field can have empty value or not.
+ ///
+ [JsonProperty("canBeEmpty")]
+ public bool CanBeEmpty { get; set; }
+
+ ///
+ /// Text that is shown when the custom field has no value.
+ ///
+ [JsonProperty("emptyText")]
+ public string EmptyText { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs b/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs
new file mode 100644
index 00000000..b6c533b3
--- /dev/null
+++ b/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using YouTrackSharp.Management;
+
+namespace YouTrackSharp.Projects
+{
+ ///
+ /// A class that represents a REST API client for methods related to operations with custom fields of a project.
+ /// It uses a implementation to connect to the remote YouTrack server instance.
+ ///
+ public class ProjectCustomFieldsService
+ {
+ private readonly Connection _connection;
+
+ ///
+ /// Creates an instance of the class.
+ ///
+ /// A instance that provides a connection to the remote YouTrack server instance.
+ public ProjectCustomFieldsService(Connection connection)
+ {
+ _connection = connection ?? throw new ArgumentNullException(nameof(connection));
+ }
+
+ ///
+ /// Get custom fields used in a project.
+ ///
+ /// Uses the REST API Get Project Custom Fields.
+ /// Id of the project to get the custom fields for.
+ /// A of that are accessible for currently logged in user.
+ /// When the call to the remote YouTrack server instance failed.
+ public async Task> GetProjectCustomFields(string projectId)
+ {
+ var client = await _connection.GetAuthenticatedHttpClient();
+ var response = await client.GetAsync($"rest/admin/project/{projectId}/customfield");
+
+ response.EnsureSuccessStatusCode();
+
+ return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync());
+ }
+
+ ///
+ /// Get a project's custom field by its name.
+ ///
+ /// Uses the REST API Get Project Custom Field.
+ /// Id of the project to get the custom field for.
+ /// Name of the custom field to get.
+ /// .
+ /// When the call to the remote YouTrack server instance failed.
+ public async Task GetProjectCustomField(string projectId, string customFieldName)
+ {
+ var client = await _connection.GetAuthenticatedHttpClient();
+ var response = await client.GetAsync($"rest/admin/project/{projectId}/customfield/{customFieldName}");
+
+ response.EnsureSuccessStatusCode();
+
+ return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync());
+ }
+
+ ///
+ /// Remove specified custom field from a project.
+ ///
+ /// Uses the REST API Delete a project custom field.
+ /// Id of the project to delete the custom field for.
+ /// Name of the custom field to delete.
+ /// When the is null or empty.
+ /// When the is null or empty.
+ /// When the call to the remote YouTrack server instance failed.
+ public async Task DeleteProjectCustomField(string projectId, string customFieldName)
+ {
+ if (string.IsNullOrEmpty(projectId))
+ {
+ throw new ArgumentNullException(nameof(projectId));
+ }
+ if (string.IsNullOrEmpty(customFieldName))
+ {
+ throw new ArgumentNullException(nameof(customFieldName));
+ }
+
+ var client = await _connection.GetAuthenticatedHttpClient();
+ var response = await client.DeleteAsync($"rest/admin/project/{projectId}/customfield/{customFieldName}");
+
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ return;
+ }
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ ///
+ /// Adds an existing custom field to a specific project.
+ ///
+ /// Uses the REST API Create a project custom field.
+ /// Id of the project to add a custom field.
+ /// to add to the project.
+ /// When the is null or empty.
+ /// When the is null or empty.
+ /// When the call to the remote YouTrack server instance failed and YouTrack reported an error message.
+ /// When the call to the remote YouTrack server instance failed.
+ public async Task CreateProjectCustomField(string projectId, CustomField customField)
+ {
+ if (string.IsNullOrEmpty(customField?.Name))
+ {
+ throw new ArgumentNullException(nameof(customField));
+ }
+
+ string query = string.Empty;
+ if (!string.IsNullOrEmpty(customField.EmptyText))
+ {
+ query = $"?emptyFieldText={WebUtility.UrlEncode(customField.EmptyText)}";
+ }
+
+ var client = await _connection.GetAuthenticatedHttpClient();
+ var response = await client.PutAsync($"rest/admin/project/{projectId}/customfield/{customField.Name}{query}", new MultipartContent());
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ ///
+ /// Updates a custom field to a specific project.
+ ///
+ /// Uses the REST API Updates a project custom field.
+ /// Id of the project.
+ /// to update in project.
+ /// When the is null or empty.
+ /// When the is null or empty.
+ /// When the call to the remote YouTrack server instance failed and YouTrack reported an error message.
+ /// When the call to the remote YouTrack server instance failed.
+ public async Task UpdateProjectCustomField(string projectId, CustomField customField)
+ {
+ if (string.IsNullOrEmpty(projectId))
+ {
+ throw new ArgumentNullException(nameof(projectId));
+ }
+
+ if (string.IsNullOrEmpty(customField?.Name))
+ {
+ throw new ArgumentNullException(nameof(customField));
+ }
+
+ string query = string.Empty;
+ if (!string.IsNullOrEmpty(customField.EmptyText))
+ {
+ query = $"?emptyFieldText={WebUtility.UrlEncode(customField.EmptyText)}";
+ }
+
+ var client = await _connection.GetAuthenticatedHttpClient();
+ var response = await client.PostAsync($"rest/admin/project/{projectId}/customfield/{customField.Name}{query}", new MultipartContent());
+
+ response.EnsureSuccessStatusCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs b/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs
index b51407ba..949498ae 100644
--- a/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs
+++ b/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs
@@ -30,7 +30,7 @@ public TimeTrackingService(Connection connection)
/// Get work types for a specific project from the server.
///
/// Uses the REST API GET Work Types for a Project.
- /// Id of the issue to get work items for.
+ /// Id of the project to get work items for.
/// An of for the requested project .
/// When the is null or empty.
/// When the call to the remote YouTrack server instance failed.
diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs
new file mode 100644
index 00000000..7a3c47f2
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using YouTrackSharp.Projects;
+using YouTrackSharp.Tests.Infrastructure;
+
+namespace YouTrackSharp.Tests.Integration.Projects
+{
+ public partial class ProjectCustomFieldsServiceTests
+ {
+ public class CreateProjectCustomField
+ {
+ [Fact]
+ public async Task Valid_Connection_Creates_CustomField_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Password;
+ var service = connection.ProjectCustomFieldsService();
+ var customField = new CustomField {Name = "TestField"};
+ var projectId = "DP1";
+
+ // Act
+ await service.CreateProjectCustomField(projectId, customField);
+
+ var created = await service.GetProjectCustomField(projectId, customField.Name);
+
+ // Assert
+ Assert.NotNull(created);
+
+ Assert.Equal(customField.Name, created.Name);
+ Assert.Equal(customField.EmptyText, string.Empty);
+
+ // cleanup
+ await service.DeleteProjectCustomField(projectId, customField.Name);
+ }
+
+ [Fact]
+ public async Task Valid_Connection_Creates_CustomField_With_EmptyText_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Password;
+ var service = connection.ProjectCustomFieldsService();
+ var customField = new CustomField { Name = "TestField", EmptyText = "empty" };
+ var projectId = "DP1";
+
+ // Act
+ await service.CreateProjectCustomField(projectId, customField);
+
+ var created = await service.GetProjectCustomField(projectId, customField.Name);
+
+ // Assert
+ Assert.NotNull(created);
+
+ Assert.Equal(customField.Name, created.Name);
+ Assert.Equal(customField.EmptyText, created.EmptyText);
+
+ // cleanup
+ await service.DeleteProjectCustomField(projectId, customField.Name);
+ }
+
+ [Fact]
+ public async Task Invalid_Connection_Throws_UnauthorizedConnectionException()
+ {
+ // Arrange
+ var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await service.GetProjectCustomField("DP1", "TestField"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs
new file mode 100644
index 00000000..da67abcc
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using YouTrackSharp.Tests.Infrastructure;
+
+namespace YouTrackSharp.Tests.Integration.Projects
+{
+ public partial class ProjectCustomFieldsServiceTests
+ {
+ public class DeleteProjectCustomField
+ {
+ [Fact]
+ public async Task Valid_Connection_Deletes_CustomField_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Token;
+ var service = connection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ var acted = false;
+ await service.DeleteProjectCustomField("DP1", " TestField");
+ acted = true;
+
+ Assert.True(acted);
+ }
+
+ [Fact]
+ public async Task Invalid_Connection_Throws_UnauthorizedConnectionException()
+ {
+ // Arrange
+ var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await service.DeleteProjectCustomField("DP1", "TestField"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs
new file mode 100644
index 00000000..153aaa18
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using YouTrackSharp.Tests.Infrastructure;
+
+namespace YouTrackSharp.Tests.Integration.Projects
+{
+ public partial class ProjectCustomFieldsServiceTests
+ {
+ public class GetProjectCustomField
+ {
+ [Fact]
+ public async Task Valid_Connection_Gets_CustomFields_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Token;
+ var service = connection.ProjectCustomFieldsService();
+ string customFieldName = "Assignee";
+
+ // Act
+ var result = await service.GetProjectCustomField("DP1", customFieldName);
+
+ // Assert
+ Assert.NotNull(result);
+
+ Assert.Equal(customFieldName, result.Name);
+ Assert.Equal(string.Empty, result.Url);
+ Assert.Equal("user[1]", result.Type);
+ Assert.True(result.CanBeEmpty);
+ Assert.Equal("Unassigned", result.EmptyText);
+ }
+
+ [Fact]
+ public async Task Invalid_Connection_Throws_UnauthorizedConnectionException()
+ {
+ // Arrange
+ var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await service.GetProjectCustomField("DP1", "Assignee"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs
new file mode 100644
index 00000000..198c56c4
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using YouTrackSharp.Projects;
+using YouTrackSharp.Tests.Infrastructure;
+
+namespace YouTrackSharp.Tests.Integration.Projects
+{
+ public partial class ProjectCustomFieldsServiceTests
+ {
+ public class GetProjectCustomFields
+ {
+ [Fact]
+ public async Task Valid_Connection_Gets_CustomFields_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Token;
+ var service = connection.ProjectCustomFieldsService();
+
+ // Act
+ var result = await service.GetProjectCustomFields("DP1");
+
+ // Assert
+ Assert.NotEmpty(result);
+ foreach (var customField in result)
+ {
+ Assert.NotNull(customField);
+ Assert.NotEqual(string.Empty, customField.Name);
+ Assert.NotEqual(string.Empty, customField.Url);
+ }
+ }
+
+ [Fact]
+ public async Task Invalid_Connection_Throws_UnauthorizedConnectionException()
+ {
+ // Arrange
+ var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await service.GetProjectCustomFields("DP1"));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs
new file mode 100644
index 00000000..d320339e
--- /dev/null
+++ b/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+using YouTrackSharp.Projects;
+using YouTrackSharp.Tests.Infrastructure;
+
+namespace YouTrackSharp.Tests.Integration.Projects
+{
+ public partial class ProjectCustomFieldsServiceTests
+ {
+ public class UpdateProjectCustomField
+ {
+ [Fact]
+ public async Task Valid_Connection_Update_CustomField_For_Project()
+ {
+ // Arrange
+ var connection = Connections.Demo1Password;
+ var service = connection.ProjectCustomFieldsService();
+ var customField = new CustomField {Name = "TestField", EmptyText = "my empty"};
+ var projectId = "DP1";
+
+ // Act
+ await service.UpdateProjectCustomField(projectId, customField);
+
+ var created = await service.GetProjectCustomField(projectId, customField.Name);
+
+ // Assert
+ Assert.NotNull(created);
+
+ Assert.Equal(customField.Name, created.Name);
+ Assert.Equal(customField.EmptyText, created.EmptyText);
+ }
+
+ [Fact]
+ public async Task Invalid_Connection_Throws_UnauthorizedConnectionException()
+ {
+ // Arrange
+ var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await service.GetProjectCustomField("DP1", "TestField"));
+ }
+ }
+ }
+}
\ No newline at end of file