Перейти к содержанию

Описание скриптов

Скрипт — это набор инструкций на C#, которые составляют тело скрытого внутреннего метода, затем компилируемого и выполняемого по триггерам.

Технически скрипты транслируются на внутренний промежуточный язык .NET - CIL.

Для работы скриптов открыт прямой доступ к Public API, используемый веб-клиент которого сгенерирован проектом Kiota.

Прямой доступ позволит пользователю писать уже сейчас все, что предоставляет публичный API.

Скрипты вызываются событиями в системе (триггеры CRUD для сущностей). На одно событие в рамках пространства может быть создано несколько скриптов.

Каждое событие имеет свой контекст - набор данных, связанный с ним: идентификаторы текущего пространства, пользователя, задачи, значения их полей и пр. Контекст для разных событий отличается. Как объект он передаётся в память выполняющегося скрипта в виде типизированного объекта (то есть, не содержит лишних свойств, а те что есть, сразу имеют правильный тип). Доступ осуществляется по краткому идентификатору “e”.

Например: var userId = e.UserId.

Сигнатуры методов имеют своими аргументами идентификаторы или имена/ключи задач, пространств, папок и т.д., там где это применимо, но они, как правило, необязательные. Если что-либо пропущено, то подразумевается текущее пространство, текущая задача и т.п., а для удобства использования они перечислены в порядке возрастания иерархии, то есть:

GetWorkitem(string? workitemKey = null, string? workspaceKey = null);

Функции-обертки

Метод, в котором выполняется пользовательский код, технически находится в классе, наследуемом от базового класса, имеющей с одной стороны, веб-клиент CwmPublicAPI, а с другой - некоторые обертки над ним, более удобные для пользовательского кода.

На данный момент реализованы следующие функции-обертки:

//    *** CREATE ***
Task<WorkspaceModel> CreateWorkspace(string name, string key, string? description = null)
Task<FolderModel> CreateFolder(string name, Guid? parentId = null, string? workspaceKey = null)
Task<TypeModel> CreateType(CreateTypeRequestBody body, string? workspaceKey = null)
Task<StatusModel> CreateStatus(CreateStatusRequestBody body, string? workspaceKey = null)
Task<WorkitemModel> CreateWorkitem(CreateWorkitemRequestBody body, string? workspaceKey = null)
Task<CommentModel> CreateWorkitemComment(string text, string? workitemKey = null, string? workspaceKey = null)
Task<PortfolioModel> CreatePortfolio(string name, Guid folderId, string? workspaceKey = null)
Task<PortfolioElementModel> CreatePortfolioElement(CreatePortfolioElementRequestBody body, string? workspaceKey = null)
Task<RoleModel> CreateRole(string name, List<Permission?>? permissions, string? workspaceKey = null)
Task<AgileModel> CreateAgile(CreateAgileRequestBody body, string? workspaceKey = null)
Task<SprintModel> CreateSprint(CreateSprintRequestBody body, string? workspaceKey = null)

// атрибуты сейчас создаются только непривязанные - не позволяет Public API
Task<AttributeModel> CreateNumberAttribute(string name, string? description, string? workspaceKey = null)
Task<AttributeModel> CreateUniStringAttribute(string name, string? description = null, string? workspaceKey = null)
Task<AttributeModel> CreateUserAttribute(string name, string? description = null, string? workspaceKey = null)
Task<AttributeModel> CreateDateAttribute(string name, string? description = null, string? workspaceKey = null)
Task<AttributeModel> CreateTimeAttribute( string name, string? description = null, string? workspaceKey = null)
Task<AttributeModel> CreateUniSelectAttribute(string name, IEnumerable<string> listOptions, string? description = null, string? workspaceKey = null)
Task<AttributeModel> CreateTagAttribute(string name, LIEnumerablest<string> listOptions, string? description = null, string? workspaceKey = null)

//   *** DELETE ***
Task DeleteWorkspace(string? workspaceKey = null)
Task DeleteFolder(Guid folderId, string? workspaceKey = null)
Task DeleteWorkitem(string? workitemKey = null, string? workspaceKey = null)
Task DeleteType(Guid typeId, string? workspaceKey = null)
Task DeleteType(string? type, string? workspaceKey = null)
Task DeleteAttribute(Guid attributeId, string? workspaceKey = null)
Task DeleteUserRole(Guid userId, Guid roleId, string? workspaceKey = null)
Task DeleteGroupRole(Guid groupId, Guid roleId, string? workspaceKey = null)
Task DeletePortfolio(Guid portfolioId, string? workspaceKey = null)
Task DeletePortfolioElement(Guid elementId, string? workspaceKey = null)
Task DeleteAgile(Guid agileExtensionId, string? workspaceKey = null)
Task DeleteSprint(Guid sprintId, string? workspaceKey = null)
Task DeleteAttachment(Guid attachmentId, string? workitemKey = null, string? workspaceKey = null)

//    *** GET ***
Task<List<WorkspaceModel>?> GetWorkspaces()
Task<WorkspaceModel?> GetWorkspace(Guid workspaceId)
Task<WorkspaceModel?> GetWorkspace(string workspaceKey = null)

Task<List<FolderModel>?> GetFolders(string? workspaceKey = null)
Task<List<FolderModel>?> GetFolderByParent(Guid parentId, string? workspaceKey = null)
Task<FolderModel?> GetFolder(Guid folderId, string? workspaceKey = null)
Task<FolderModel?> GetFolder(string name, string? workspaceKey = null)

Task<List<WorkitemModel>?> GetWorkitems(string? workspaceKey = null)
Task<WorkitemModel?> GetWorkitem(Guid workitemId, string? workspaceKey = null)
Task<WorkitemModel?> GetWorkitem(string? workitemKey = null, string? workspaceKey = null)
Task<List<WorkitemModel>?> GetWorkitemByParent(bool withSubitems = false, string? parentKey = null, string? workspaceKey = null) // планарный список для иерархии потомков (withSubitems = true)
Task<WorkitemModelTree?> GetWorkitemTree(Guid? parentId = null, string? workspaceKey = null) // потомки в виде иерархического дерева, WorkitemModelTree - собственный тип ScriptAPI (https://git.testit.ru/teamstorm/teamstorm/-/blob/feature/ib/TS-8917-scripts-child-tasks/src/backend/dotnet/AutomationService.ScriptGate/Contracts/Tree.cs?ref_type=heads)

Task<List<AttachmentModel>?> GetWorkitemAttachments(string? workitemKey = null, string? workspaceKey = null)
Task<AttachmentModel?> GetWorkitemAttachment(Guid attachmentId, string? workitemKey = null, string? workspaceKey = null)

Task<List<CommentModel>?> GetWorkitemComments(string? workitemKey = null, string? workspaceKey = null)

Task<PortfolioModel> CreatePortfolio(string name, Guid folderId, string? workspaceKey = null)
Task<PortfolioElementModel> CreatePortfolioElement(CreatePortfolioElementRequestBody body, string? workspaceKey = null)

Task<List<UserModel>?> GetUsers()
Task<UserModel?> GetUser(Guid userId)
Task<UserModel?> GetUser(string userName)
Task<List<UserModel>?> GetUsersByRole(Guid roleId, string? workspaceKey = null)

Task<List<GroupModel>?> GetUsersGroups()
Task<GroupModel?> GetUserGroup(Guid userGroupId)
Task<List<GroupModel>?> GetUsersGroups(string userName)

Task<List<RoleModel>?> GetRoles(string? workspaceKey = null)
Task<List<RoleModel>?> GetRoles(bool? isSystem, string? workspaceKey = null)
Task<RoleModel?> GetRole(Guid roleId, string? workspaceKey = null)
Task<RoleModel?> GetRole(string role, string? workspaceKey = null)

Task<List<TypeModel>?> GetTypes(string? workspaceKey = null)
Task<TypeModel?> GetType(Guid typeId, string? workspaceKey = null)
Task<TypeModel?> GetType(string typeName, string? workspaceKey = null)

Task<List<LinkTypeModel>?> GetLinkTypes(string? workspaceKey = null)

Task<List<StatusCategoryModel>?> GetStatusCategories()
Task<List<StatusModel>?> GetStatuses(string? workspaceKey = null)
Task<StatusModel?> GetStatus(Guid statusId, string? workspaceKey = null)
Task<StatusModel?> GetStatus(string name, string? workspaceKey = null)

Task<List<AttributeModel>?> GetAttributes(string? workspaceKey = null)
Task<AttributeModel?> GetAttribute(Guid attributeId, string? workspaceKey = null)
Task<AttributeModel?> GetAttribute(string attributeName, string? workspaceKey = null)

Task<NumberFieldValueModel?> GetNumberAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<NumberFieldValueModel?> GetNumberAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<UniStringFieldValueModel?> GetUniStringAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<UniStringFieldValueModel?> GetUniStringAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<UniSelectFieldValueModel?> GetUniSelectAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<UniSelectFieldValueModel?> GetUniSelectAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<DateFieldValueModel?> GetDateAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<DateFieldValueModel?> GetDateAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<TimeFieldValueModel?> GetTimeAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<TimeFieldValueModel?> GetTimeAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<TagFieldValueModel?> GetTagAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<TagFieldValueModel?> GetTagAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<UserFieldValueModel?> GetUserAttributeValue(Guid attributeId, string? workitemKey = null, string? workspaceKey = null)
Task<UserFieldValueModel?> GetUserAttributeValue(string attributeName, string? workitemKey = null, string? workspaceKey = null)

Task<List<PortfolioModel>?> GetPortfolios(string? workspaceKey = null)
Task<List<PortfolioModel>?> GetPortfoliosInFolder(Guid folderId, string? workspaceKey = null)
Task<PortfolioModel?> GetPortfolio(Guid portfolioId, string? workspaceKey = null)
Task<PortfolioModel?> GetPortfolio(string name, string? workspaceKey = null)

Task<List<PortfolioElementModel>?> GetPortfolioElements(string? workspaceKey = null)
Task<List<PortfolioElementModel>?> GetPortfolioElementsInFolder(Guid folderId, string? workspaceKey = null)
Task<List<PortfolioElementModel>?> GetPortfolioElementsForStatus(string status, string? workspaceKey = null)
Task<List<PortfolioElementModel>?> GetPortfolioElements(Guid portfolioId, string? workspaceKey = null)
Task<PortfolioElementModel?> GetPortfolioElement(Guid elementId, string? workspaceKey = null)
Task<PortfolioElementModel?> GetPortfolioElement(string elementName, string? workspaceKey = null)

Task<List<WorkitemModel>?> GetWorkitemsByPortfolioElement(Guid elementId, string? workspaceKey = null)

Task<List<TimeTrackingEntryModel>?> GetTimeTrackingEntries(DateTime? startDate = null, DateTime? endDate = null,
    List<string>? users = null, int? maxItemsCount = 50, string? fromToken = null)

Task<List<AgileModel>?> GetAgileExtensions(string? workspaceKey = null)
Task<AgileModel?> GetAgileExtension(Guid agileExtensionId, string? workspaceKey = null)

Task<List<SprintModel>?> GetSprints(string? workspaceKey = null)
Task<SprintModel?> GetSprint(Guid sprintId, string? workspaceKey = null)
Task<SprintModel?> GetSprint(string name, string? workspaceKey = null)

//    *** UPDATE ***
Task<WorkspaceModel> UpdateWorkspaceName(string name, string? workspaceKey = null)

Task<FolderModel?> UpdateFolder(PatchFolderRequestBody body, Guid folderId, string? workspaceKey = null) // общего вида - сразу все свойства скопом
Task<WorkitemModel> UpdateFolderName(string name, Guid folderId, string? workspaceKey = null)

Task<PortfolioModel> UpdatePortfolio(Guid portfolioId, string name, string? workspaceKey = null)
Task<PortfolioElementModel> UpdatePortfolioElement(PortfolioElementModel body, string? workspaceKey = null)

Task<WorkitemModel?> UpdateWorkitem(PatchWorkitemRequestBody body, string? workitemKey = null, string? workspaceKey = null) // общего вида
Task<WorkitemModel> UpdateWorkitemParent(Guid? parentId, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemStatus(string status, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemType(string typeName, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemName(string name, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemDescription(string desc, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemAssignee(Guid userId, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemAssignee(string? userName, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemStartDate(DateTime? startDate, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemDueDate(DateTime? dueDate, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemSprint(Guid? sprintId, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemStoryPoints(int? points, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemOriginalEstimate(int? estimate, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemWorkflow(Guid? workflowId, string? workitemKey = null, string? workspaceKey = null)
Task<WorkitemModel> UpdateWorkitemPortfolioElement(List<Guid?>? elementIds, string? workitemKey = null, string? workspaceKey = null)

// атрибуты (только непривязанные - привязывать к типу воркайтема(задачи) не позволяет PublicAPI)
Task<AttributeModel> UpdateWorkitemAttribute(Guid attributeId, PatchAttributeRequestBody body, string? workspaceKey = null) // общего вида
Task<AttributeModel> UpdateScalarWorkitemAttribute(Guid attributeId, string name, string? description = null, string? workspaceKey = null) // все скалярные
Task<AttributeModel> UpdateListWorkitemAttribute(Guid attributeId, string name, List<PatchAttributeOptionModel> listOptions,
        string? description = null, string? workspaceKey = null) // UniSelect & Tag

// значения атрибутов
Task<UniStringFieldValueModel?> UpdateUniStringAttributeValue(string attributeName, string value, string? workitemKey = null, string? workspaceKey = null)
Task<UniStringFieldValueModel?> UpdateUniStringAttributeValue(Guid attributeId, string value, string? workitemKey = null, string? workspaceKey = null)

Task<NumberFieldValueModel?> UpdateNumberAttributeValue(string attributeName, double? value, string? workitemKey = null, string? workspaceKey = null)
Task<NumberFieldValueModel?> UpdateNumberAttributeValue(Guid attributeId, double? value, string? workitemKey = null, string? workspaceKey = null)

Task<DateFieldValueModel?> UpdateDateAttributeValue(string attributeName, DateTime? value, string? workitemKey = null, string? workspaceKey = null)
Task<DateFieldValueModel?> UpdateDateAttributeValue(Guid attributeId, DateTime? value, string? workitemKey = null, string? workspaceKey = null)

Task<TimeFieldValueModel?> UpdateTimeAttributeValue(string attributeName, int? seconds, string? workitemKey = null, string? workspaceKey = null)
Task<TimeFieldValueModel?> UpdateTimeAttributeValue(Guid attributeId, int? seconds, string? workitemKey = null, string? workspaceKey = null)

Task<UserFieldValueModel?> UpdateUserAttributeValue(string attributeName, Guid userId, string? workitemKey = null, string? workspaceKey = null)
Task<UserFieldValueModel?> UpdateUserAttributeValue(Guid attributeId, Guid userId, string? workitemKey = null, string? workspaceKey = null)

Task<UniSelectFieldValueModel?> UpdateUniSelectAttributeValue(string attributeName, string option, string? workitemKey = null, string? workspaceKey = null)
Task<UniSelectFieldValueModel?> UpdateUniSelectAttributeValue(Guid attributeId, string option, string? workitemKey = null, string? workspaceKey = null)

Task<TagFieldValueModel?> UpdateTagAttributeValue(string attributeName, List<string> options, string? workitemKey = null, string? workspaceKey = null)
Task<TagFieldValueModel?> UpdateTagAttributeValue(Guid attributeId, List<string> options, string? workitemKey = null, string? workspaceKey = null)

//   *** MISC ***
Task AddUserRole(Guid userId, Guid roleId, string? workspaceKey = null)
Task<RoleModel> UpdateRolePermissions(Guid roleId, List<Permission?>? permissions = null, string? workspaceKey = null)

Примеры использования оберток см. в подразделе Примеры.

Примеры

Примечание

Для всех функций ScriptAPI (доступ к PublicAPI и логирование) необходимо указывать впереди ключевое слово await, так как они асинхронные.

Синтаксис языка — C11, поэтому инициализация коллекций происходит следующим образом: List<int> list = new { 1, 2 }, а не List<int> list = [ 1, 2 ].

Простейший пример скрипта, который даже не использует PublicAPI:

debug("USER = " + e.UserId);

Примечание

Если тело скрипта передается в запросе напрямую (на странице Swagger или утилите типа Postman), все внутренние двойные кавычки свойства, передаваемого JSON, необходимо экранировать:

{
   Name: "Script1",
   ScriptType: "Method",
   EventType: "WorkitemNameChanged",
   Script: "debug(\"USER = \" + e.UserId);", // вот тут в тексте самого скрипта - экранирование кавычек
}

Пример создания папки со случайным именем

var r = new Random();
var newName = $"name{r.Next(0, 1000)}";
await CreateFolder(newName); // вот тут должно быть await
debug("New name = " + newName);

Примеры скриптов, созданных через обертки

// Сравнение с текущим событием можно будет делать как по его Id, так и по его имени (enum лучше)
// Кейс 1: e.EventId == Events.WorkitemCreated (Events - класс с полями в виде именованных гуидов + некоторые вспомогательные функции)
// Кейс 2: e.EventType == EventTypes.WorkitemCreated (EventTypes - enum)
// Events также имеет статический словарь '<Guid, EventTypes> Map' - Id и enum события для системных полей
// Имена обсуждаемы

// Сценарий: автозаполнение исполнителя в зависимости от типа запроса
var wiType = await GetType(e.WorkitemTypeId);
if (e.EventType == EventTypes.WorkitemCreated && wiType.Name == "Bug") {
    // UserModel? assignee = await GetUser("ivan.ivanov"); // в данном случае получение модели излишне
    WorkitemModel? workitem = await UpdateWorkitemAssignee("ivan.ivanov");
}

//  Сценарий: по умолчанию проставление среднего приоритета при регистрации запроса
var wiType = await GetType(e.WorkitemTypeId);
if (e.EventId == EventTypes.WorkitemCreated && wiType.Name == "Bug") {
    // если уже имеется Id атрибута, лучше использовать функцию, принимающую именно его, а не имя (производительность)
    await UpdateUniSelectAttributeValue("Приоритет", "Средний"); // текущий воркайтем
    //await SetUniSelectAttributeValue(attributeId, "Средний", workitemId2); // сторонний воркайтем, но текущего воркспейса
    //await SetUniSelectAttributeValue(attributeId, "Средний", workitemId2, workspaceId2); // воркайтем в стороннем воркспейсе
}

// Сценарий: проставление команды в зависимости от исполнителя
var user = await GetUser(e.WorkitemAssigneeId);
// 'WorkitemChanged' нет - все события на изменение гранулярные 
if (e.EventType == EventTypes.WorkitemNameChanged && user.Username == "ivan.ivanov") {
    await UpdateUniSelectAttributeValue("Команда", "Alfa");
}

// Сценарий: Автоматическое назначение на задачу определенной проектной роли. Как следствие назначается пользователь, у которого установлена такая проектная роль.
List<UserModel> users = await GetUsersByRole(roleId); // можно так (все функции планарно на одном уровне, но функции могут быть длиннее по имени - с префиксом сущности)
// List<UserModel> users = await WorkspaceUsers.FindByRole(role); // а можно эдак (в АПИ организовать отдельные 'свойства доступа': для текущего воркайтема, общего доступа в воркспейсам, юзерам и т.д.)

// Сценарий: Автоматическое присвоение типа документа по вхождению определенного текста в название документа. 
// Сценарий: Создание задачи с автоматическим определением типа на базе локации пользователя, то есть где пользователь инициировал создание задачи. Чаще всего применимо к спискам (портфолио).
WorkitemModel wi = await UpdateWorkitemType("SomeType"); // текущему воркайтему

// Сценарий: Расчет и заполнение атрибута в зависимости от значений других атрибутов текущей задачи. Например, автоматическое присваивание задаче портфолио, если она является определенного типа.
PortfolioElementModel? elem1 = await GetPortfolioElement(elementId);
PortfolioElementModel? elem1 = await GetPortfolioElement("Element1"); // или по elementName
// PorfolioElementModel? elem1 = await PorfolioElements.FindByName(name); // либо так
elem1.StartDate = DateTimeOffset.UtcNow.AddDays(10).Date; // изменить сам элемент
await UpdatePortfolioElement(elem1);
await UpdateWorkitemPortfolioElements(elementIds); // привязка к воркайтему списка элементов портфолио - по гуидам

// Сценарий: Установить ответственного при определённых значениях
var priorityValue = await GetUniSelectAttributeValue(priorityAttributeName);
var tagValue = await GetTagAttributeValue(tagAttributeName); // текущий воркайтем
if (priorityValue.Value.Name == "Высокий" && tagValue.Value.Any(a => a.Name == "UX")) {
    await UpdateWorkitemAssignee(userId);  // текущему воркайтему!
}

// Автоматически рассчитать стоимость задачи и записать результат в атрибут "Общая стоимость", перемножив значения атрибутов "Оценка в часах" и "Стоимость часа".
NumberFieldValueModel attr1 = await GetNumberAttributeValue("Оценка в часах");
NumberFieldValueModel attr2 = await GetNumberAttributeValue("Стоимость часа");
if(attr1.Value != null && attr2.Value != null)
{
    var val3 = attr1.Value * attr2.Value;
    await UpdateNumberAttributeValue("Общая стоимость", val3);
}

// Автоматически рассчитать сумму атрибутов “Оценка” во всех дочерних задачах и заполнить рассчитанным значением атрибута “Оценка“ в родительской задаче.
var tree = await GetWorkitemTree();
List<WorkitemModel> sons = tree.GetChildren();
var result = 0.0;
foreach (var son in sons)
{
    var attribute = son.Attributes.Single(a => a.NumberFieldValueModel.Name == "Оценка");
    result += attribute.NumberFieldValueModel.Value ?? 0;
}

await UpdateNumberAttributeValue("Оценка", result);

Примеры создания скриптов напрямую

// Пример обёртки создания пространства
public async Task<WorkspaceModel> CreateWorkspace(string name, string key, string? description = null)
{
   var response = await Latest
    .Workspaces
    .PostAsync(new CreateWorkspaceRequestBody
    {
        Key = key,
        Name = name,
        Description = description,
    });

   if (response is null)
   {
    throw new OperationFailedException($"Failed to create the workspace with name [{name}] and key [{key}]");
   }

   return response;
}

// создание задачи (при этом заранее создаём объект задачи в body)
public async Task<WorkitemModel> CreateWorkitem(CreateWorkitemRequestBody body, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);

    return await Latest
    .Workspaces[workspaceKey]
    .Workitems
    .PostAsync(body);
}

// создание комментария к задаче
public async Task<CommentModel> CreateWorkitemComment(string text, string? workitemKey = null, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);
    CheckWorkitemKey(ref workitemKey);

    var body = new CreateCommentRequestBody { Text = text };

    return await Latest
    .Workspaces[workspaceKey]
    .Workitems[workitemKey]
    .Comments
    .PostAsync(body);
}

// получение пространств
public async Task<List<WorkspaceModel>?> GetWorkspaces()
{
    var response = await Latest
        .Workspaces
        .GetAsync();
    return response?.Items;
}

// Получение модели папки
public async Task<FolderModel?> GetFolder(Guid folderId, string? workspaceKey = null)
{
   CheckWorkspaceKey(ref workspaceKey);

   return await Latest
    .Workspaces[workspaceKey]
    .Folders[folderId]
    .GetAsync();
}

// Получение статусов
public async Task<List<StatusModel>?> GetStatuses(string? workspaceKey = null)
{
   CheckWorkspaceKey(ref workspaceKey);

   var response = await Latest
    .Workspaces[workspaceKey]
    .Statuses
    .GetAsync();
   return response?.Items;
}

// получение атрибута
public async Task<AttributeModel?> GetAttribute(string attributeName, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);

    var response = await Latest
        .Workspaces[workspaceKey]
        .Attributes
        .GetAsync(x =>
           x.QueryParameters.Name = attributeName);
    return response?.Items?.FirstOrDefault();
}

// Получение значений атрибутов указанной задачи:
AttributeValueModelList? response = await Latest
    .Workspaces[workspaceKey]
    .Workitems[workitemKey]
    .Attributes
    .GetAsync();
return response;

// Изменение имени задачи:
var body = new PatchWorkitemRequestBody { Name = name };
var patchResponse = await Latest
    .Workspaces[workspaceKey]
    .Workitems[workitemKey]
    .PatchAsync(body);

// изменение значения атрибута типа "Время" для указанной задачи
public async Task<TimeFieldValueModel?> UpdateTimeAttributeValue(Guid attributeId, int? seconds,
    string? workitemKey = null, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);
    CheckWorkitemKey(ref workitemKey);

    var body = new WithAttributeItemRequestBuilder.WithAttributePutRequestBody
    {
    UpdateTimeFieldRequestBody = new UpdateTimeFieldRequestBody
    {
        Type = AttributeType.TimeDuration,
        Value = seconds,
    },
    };

    var response = await Latest
     .Workspaces[workspaceKey]
     .Workitems[workitemKey]
     .Attributes[attributeId]
     .PutAsync(body);

    return response?.TimeFieldValueModel;
}

// Изменение портфолио (по факту - просто имени):
public async Task<PortfolioModel> UpdatePortfolio(Guid portfolioId, string name, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);

    var patchResponse = await Latest
    .Workspaces[workspaceKey]
    .Portfolios[portfolioId]
    .PatchAsync(new PatchPortfolioRequestBody
        {
        Name = name,
        });

    if (patchResponse is null)
    {
        throw new OperationFailedException($"Failed to update the portfolio in workspace {workspaceKey}");
    }

    return patchResponse;
}

// добавить роль пользователю:
public async Task AddUserRole(Guid userId, Guid roleId, string? workspaceKey = null)
{
    CheckWorkspaceKey(ref workspaceKey);

    await Latest
    .Workspaces[workspaceKey]
    .Users[userId]
    .Roles[roleId]
    .PostAsync();
}

// Удаление атрибута:
public async Task DeleteAttribute(Guid attributeId, string? workspaceKey = null)
{
   CheckWorkspaceKey(ref workspaceKey);

   await Latest
    .Workspaces[workspaceKey]
    .Attributes[attributeId]
    .DeleteAsync();
}