Перейти к содержимому

SDK (Portal.WebPart.Sdk)

C# SDK для серверного рендеринга WebPart (hybrid SSR).

Настройка SDK: скачайте Extension SDK с korport.ru/developers (версия = версия Portal). Для разработки в монорепозитории Portal см. backend/src/Portal.WebPart.Sdk/IWebPart.cs.

Подробное руководство с примерами UI, data-wp-action и работы с API: Интерфейс, контролы и API.

using Portal.Contracts.Lists;
using Portal.WebPart.Sdk;
public sealed class TasksWidgetWebPart : IWebPart
{
public async Task<WebPartResult> RenderAsync(IWebPartContext ctx, CancellationToken cancellationToken = default)
{
var listId = ctx.Properties.GetGuid("listId");
// Фильтрованная выборка (push-down в SQL) — предпочтительный способ
var items = await ctx.Lists.QueryItemsAsync(listId,
ListItemQueryBuilder.ForList(listId)
.WhereField("status", f => f.Eq("Новая"))
.OrderBy("__created_at", desc: true)
.Take(ctx.Properties.GetInt32("pageSize", 10))
.Build(),
cancellationToken);
var html = "<ul>" + string.Join("", items.Select(i => $"<li>{i.GetProperty("id")}</li>")) + "</ul>";
return new WebPartResult(html);
}
public Task<WebPartResult> HandleActionAsync(string action, JsonElement data, IWebPartContext ctx, CancellationToken ct = default)
=> Task.FromResult(new WebPartResult(""));
}
ЧленОписание
RenderAsync(ctx, ct)Серверный рендер HTML
HandleActionAsync(action, data, ctx, ct)Обработка data-wp-action
WebPartResult(Html, Error?)Результат: HTML и опциональная ошибка

Не копируйте локальный GetDataString. В SDK:

var form = data.AsActionData();
var title = form.GetString("title"); // wpTitle / title / title
var ok = form.GetBoolean("isActive"); // checkbox "true"/"on"
var id = form.GetGuid("itemId");
var assignee = form.GetJson("assigneeJson"); // hidden JSON (person)

Ключи wp*, snake_case и camelCase считаются одним именем. Подробнее: Интерфейс и API §2.1.

СвойствоТипОписание
UserPortalUserInfo?Текущий пользователь (или null для анонима)
NodeIdGuid?Узел текущей страницы/формы (то же, что Host.NodeId)
HostIWebPartHostКонтекст страницы или формы: список, элемент, файл
PropertiesIReadOnlyDictionary<string, JsonElement>Свойства из Property Pane
ListsIWebPartListsApiСписки и элементы
LibrariesIWebPartLibrariesApiБиблиотеки и файлы
NodesIWebPartNodesApiУзлы
RoutesIWebPartRoutesApiПостроение ссылок
GroupsIWebPartGroupsApiГруппы доступа
UsersIWebPartUsersApiПрофиль сотрудника и оргструктура
PermissionsIWebPartPermissionsApiПроверка и назначение ACL
KedoIWebPartKedoApiОтправка в КЭДО (нужен админ КЭДО) — инструкция
FieldsIWebPartFieldsFormatterФорматирование значений полей

Платформа передаёт host при render/action: на форме спискаlistId, itemRef, formMode; на страницеnodeId / pageId. Ссылки сами по себе запросов не делают; Get* загружают данные с проверкой прав.

// Форма списка /items/12/edit
if (ctx.Host.IsListForm && ctx.Host.HasList)
{
var item = await ctx.Host.GetOrNewListItemAsync(ct); // edit → #12; new → пустой
var title = item.GetString("title");
}
// Только ссылки
var listId = ctx.Host.ListId;
var itemRef = ctx.Host.ItemRef; // null на создании
var mode = ctx.Host.FormMode; // New / View / Edit
// Ленивая загрузка
var node = await ctx.Host.GetNodeAsync(ct);
var list = await ctx.Host.GetListAsync(ct);
var rawItem = await ctx.Host.GetItemAsync(ct); // JsonElement?
var portalItem = await ctx.Host.GetListItemAsync(ct); // null на new
var library = await ctx.Host.GetLibraryAsync(ct);
var file = await ctx.Host.GetFileAsync(ct);
Свойство / методОписание
KindNone / Page / ListForm / LibraryFile
NodeId / PageIdУзел и страница (на обычной странице)
ListId / ItemRefСписок и элемент формы (ItemRef — номер или GUID-строка)
LibraryId / FileRefБиблиотека и файл (если host передан)
FormModeNew / View / Edit на форме списка
HasList / HasItem / HasLibrary / HasFileЕсть ли соответствующая ссылка
IsListForm / IsNewForm / IsViewForm / IsEditFormУдобные флаги
GetNodeAsync / GetListAsync / GetItemAsyncСырой JSON или null
GetListItemAsyncPortalListItem? (null на создании)
GetOrNewListItemAsyncЭлемент для формы: load или NewListItemAsync
GetLibraryAsync / GetFileAsyncБиблиотека / файл

Формы create/edit: Кастомная форма.

СвойствоТипОписание
IdGuidUUID пользователя
LoginstringЛогин
DisplayNamestring?Отображаемое имя
IsPortalAdminboolАдминистратор портала
GroupIdsIReadOnlyList<Guid>Группы, в которых состоит пользователь

Справочник параметров и сценариев (список текущего / другого узла, dependsOn): Параметры WebPart. Методы чтения — §7.

МетодОписание
GetString(key, defaultValue = "")Строка (числа и bool приводятся к тексту)
GetGuid(key)GUID; Guid.Empty, если ключ отсутствует или невалиден
GetInt32(key, defaultValue = 0)Целое число
GetBoolean(key, defaultValue = false)Булево (true/false или строка)
МетодОписание
GetListItemAsync / NewListItemAsync / QueryListItemsAsync / GetListItemsAsyncSharePoint-подобный PortalListItem: item["title"], CreateAsync / UpdateAsync (рекомендуется)
GetAsyncМетаданные списка
FindBySlugAsync(nodeId, slug) / FindByTitleAsync(nodeId, title)Найти список → PortalListRef (Id, Slug, …)
GetFieldsAsyncСхема полей
GetItemsAsync / QueryItemsAsyncВыборка элементов (сырой JSON)
GetItemAsyncОдин элемент, сырой JSON (itemRef — номер или guid)
GetItemVersionsAsyncИстория версий (versioningEnabled, versions[] со снимками fieldValues, fields[])
GetItemVersionAsyncОдна версия по номеру со снимком fieldValues
CreateItemAsync / UpdateItemAsyncНизкоуровневое создание / обновление (fieldValues: UUID или internal_name)
МетодОписание
GetAsyncМетаданные библиотеки
FindBySlugAsync(nodeId, slug) / FindByTitleAsync(nodeId, title)Найти библиотеку → PortalLibraryRef
GetFilesAsync / QueryFilesAsyncВыборка файлов
GetFileAsync(libraryId, fileRef)Один файл/папка (номер или GUID)
DownloadUrl(fileId)URL скачивания файла
МетодОписание
GetAsyncУзел
GetChildrenAsyncДочерние узлы
FindBySlugAsync(slug, parentId?)Узел по slug (parentId: null — корень) → PortalNodeRef
МетодОписание
NodeHrefAsync(nodeId)Ссылка на узел
ListItemHrefAsync(nodeId, listId, itemNumber)Ссылка на элемент списка
ResolvePathAsync(pathOrUrl)Разбор path/URL → PortalResolvedRoute (NodeId, ListId, ItemNumber, …)
МетодОписание
GetAsync(groupId)Группа → PortalGroupInfo (GroupType, IsFromDirectory)
FindByNameAsync(name)Группа по точному имени
SearchAsync(query, limit?)Поиск по подстроке
IsMemberAsync(groupId, userId)Состоит ли пользователь в группе
GetMembersAsync(groupId)Участники группы (WebPartGroupMember: Id, Login, DisplayName)

Откуда брать id для прав и ACL: Права доступа из кода §1.

Проверка эффективных прав текущего пользователя и назначение ACL. Типы: PortalPermission, PortalAclEntry, PermissionResource, PermissionLevel (Portal.Contracts.Permissions).
Гайд: Права доступа из кода. REST: API: права.

using Portal.Contracts.Permissions;
var node = await ctx.Permissions.ForCurrentNodeAsync(ct);
if (!node.CanView) return new WebPartResult("", "Нет доступа");
var list = await ctx.Permissions.ForListAsync(listId, ct);
var item = await ctx.Permissions.ForListItemAsync(listId, "12", ct); // номер или UUID
if (item.CanEdit) { /* … */ }
var editors = await ctx.Groups.FindByNameAsync("Редакторы HR", ct)
?? throw new InvalidOperationException("Группа не найдена");
await ctx.Permissions.GrantToGroupAsync(
PermissionResource.ListItem, "12", editors.Id, PermissionLevel.Edit, listId: listId, ct: ct);
var acl = await ctx.Permissions.ListAsync(PermissionResource.ListItem, "12", listId: listId, ct: ct);
await ctx.Permissions.RevokeAsync(acl[0].Id, ct);
МетодОписание
ForCurrentNodeAsync / ForNodeAsync / ForListAsync / ForListItemAsync / ForLibraryAsync / ForFileAsync / ForPageAsync / ForSurveyAsyncЭффективные права → PortalPermission (CanViewCanDelete)
CanAsync(resourceType, resourceId, required, …)Достаточно ли уровня
GrantToUserAsync / GrantToGroupAsync / GrantAsyncUpsert ACL
ListAsyncЯвные назначения на ресурс
RevokeAsync(permissionId)Удалить запись ACL
МетодОписание
GetProfileAsync(userId)Карточка: ФИО, email, card_fields (meta по настройке ad-sync), HasAvatar, ManagerUserId (UserPublicProfile)
GetOrgChainAsync(userId)Цепочка руководителей от корня до пользователя (UserOrgChain.Chain)
GetOrgChildrenAsync(userId)Прямые подчинённые для lazy-раскрытия (UserOrgChildren.Children)
AvatarUrl(userId)URL аватара: /api/v1/users/{id}/avatar
var profile = await ctx.Users.GetProfileAsync(userId, ct);
var avatar = profile.HasAvatar
? $"<img src=\"{ctx.Users.AvatarUrl(userId)}\" alt=\"\" />"
: "";
var chain = await ctx.Users.GetOrgChainAsync(userId, ct);

Типы — Portal.Contracts.Users: UserPublicProfile, UserOrgNode, UserOrgChain, UserOrgChildren.

МетодОписание
FormatValue(field, value)Человекочитаемое представление значения поля
МетодКогда использовать
QueryListItemsAsync + ListItemQueryBuilderФильтр, сортировка, поиск → PortalListItemрекомендуется
QueryItemsAsync(listId, ListItemQuery, ct)То же, сырой JSON
GetItemsAsync / GetListItemsAsyncSugar: первая страница представления без явного фильтра

Сортировка: .OrderBy("column", desc: true) в builder. Типы — Portal.Contracts.Lists: ListItemQuery, ListItemQueryBuilder, ListItemFilterBuilder, ViewFilter.

Частые кейсы (статус, даты, person [Я], lookup, пагинация, Timer Job):
Выборка элементов списка (Query / LINQ-стиль)

// По internal_name поля (аналог SharePoint FieldRef Name='Title')
var settings = await ctx.Lists.QueryListItemsAsync(listId,
ListItemQueryBuilder.ForList(listId)
.WhereField("title", f => f.Eq("GlobalSettings"))
.Take(1)
.Build(), ct);
// С представлением + JSON-фильтром
var query = new ListItemQuery
{
ViewId = viewId,
Filter = ViewFilterJson.Parse("""{"logic":"and","conditions":[{"column":"title","operator":"eq","value":"A"}]}"""),
Limit = 50,
};
var items = await ctx.Lists.QueryItemsAsync(listId, query, ct);

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

МетодКогда использовать
QueryFilesAsync(libraryId, LibraryFileQuery, ct)Фильтр, сортировка, поиск — основной API
GetFilesAsync(libraryId, limit?, ct)Sugar: первая страница корня без явного фильтра

Сортировка: .OrderBy("updated_at", desc: true) в builder или orderBy / sort в opts.

Типы запроса — Portal.Contracts.Libraries: LibraryFileQuery, LibraryFileQueryBuilder, LibraryFileFieldFilterBuilder.

using Portal.Contracts.Libraries;
var pdfs = await ctx.Libraries.QueryFilesAsync(libraryId,
LibraryFileQueryBuilder.ForLibrary(libraryId)
.Where(b => b.Eq("item_type", "file").Eq("mime_type", "application/pdf"))
.OrderBy("updated_at", desc: true)
.Take(20)
.Build(), ct);

Рекомендуется SharePoint-подобный API (гайд):

var item = await ctx.Lists.GetListItemAsync(listId, "12", ct); // PortalListItem
var status = item.GetString("status"); // string?
var due = item.GetDate("due_at"); // string? "YYYY-MM-DD"
var room = item.GetLookup("room"); // PortalLookupValue?
item["status"] = "Готово";
await item.UpdateAsync(ct);
var created = await ctx.Lists.NewListItemAsync(listId, ct);
created["title"] = "Новая задача";
await created.CreateAsync(ct);
var found = await ctx.Lists.QueryListItemsAsync(listId,
ListItemQueryBuilder.ForList(listId)
.WhereField("status", f => f.Eq("Новая"))
.Take(50)
.Build(), ct); // IReadOnlyList<PortalListItem>

Таблица возвращаемых типов геттеров: Кастомная форма §1.

МетодОписание
GetListItemAsync / NewListItemAsync / QueryListItemsAsync / GetListItemsAsyncPortalListItem по internal_name
GetFieldsAsync(listId, ct)Схема полей (id, internal_name, field_type)
GetItemAsync(listId, itemRef, ct)Сырой JSON (itemRef — номер или guid)
GetItemVersionsAsync / GetItemVersionAsyncВерсии
CreateItemAsync / UpdateItemAsyncНизкоуровневая запись (fieldValues: UUID или internal_name)

Низкоуровневый пример (UUID, для версий / сырого JSON):

var fields = await ctx.Lists.GetFieldsAsync(listId, ct);
var statusFieldId = fields
.First(f => f.GetProperty("internal_name").GetString() == "status")
.GetProperty("id").GetString()!;
var item = await ctx.Lists.GetItemAsync(listId, "12", ct);
var status = item.GetProperty("field_values").GetProperty(statusFieldId).GetString();
await ctx.Lists.UpdateItemAsync(listId, "12", new Dictionary<string, object?>
{
[statusFieldId] = "Готово", // или ["status"] = "Готово"
}, ct);
// Значения полей в конкретной версии
var v2 = await ctx.Lists.GetItemVersionAsync(listId, "12", versionNumber: 2, ct);
var statusThen = PortalItemJson.GetFieldString(v2, statusFieldId);
// Вся история
var history = await ctx.Lists.GetItemVersionsAsync(listId, "12", ct);
foreach (var version in PortalItemJson.EnumerateVersions(history))
{
var n = PortalItemJson.GetVersionNumber(version);
var statusAt = PortalItemJson.GetFieldString(version, statusFieldId);
}

Полные примеры (REST, JS, Event Receiver, Timer Job): Значения полей списков.

WebPartTemplate загружает embedded .html из сборки WebPart и подставляет плейсхолдеры {{key}}:

МетодНазначение
Load(assembly, resourceName)Загрузить шаблон (с кэшированием)
Load<T>(resourceName)То же из сборки типа T
LoadAndApply(assembly, resourceName, values)Загрузить и подставить плейсхолдеры
Apply(template, values)Подставить плейсхолдеры в уже загруженную строку
// Статический шаблон без плейсхолдеров
var legend = WebPartTemplate.Load(typeof(MyWebPart).Assembly, "Templates.legend.html");
// или: WebPartTemplate.Load<MyWebPart>("Templates.legend.html");
// Шаблон с подстановкой
var html = WebPartTemplate.LoadAndApply(
typeof(MyWebPart).Assembly,
"Templates.widget.html",
new Dictionary<string, string> { ["title"] = WpHtml.Escape(title) });

Подробнее: Интерфейс, контролы и API — HTML-шаблоны.

Шаблон dotnet new portal-webpart создаёт Templates/widget.html и пример в MyWebPartHandler.cs.

Хелперы для разбора JSON элементов списка из API:

МетодОписание
GetItemGuid(item)Внутренний GUID (guid или legacy id-строка)
GetItemNumber(item)Публичный номер элемента (id как number)
GetItemRef(item)Ref для path API: номер или GUID-строка
TryGetFieldValues(itemOrVersion)Объект field_values / fieldValues
TryGetFieldValue(itemOrVersion, fieldId)Значение поля по UUID
GetFieldString(itemOrVersion, fieldId)Строковое значение поля
EnumerateVersions(versionsResponse)Массив версий из GetItemVersionsAsync
TryFindVersion(versionsResponse, versionNumber)Версия по номеру
GetVersionNumber(version) / IsVersioningEnabled(...)Метаданные истории
ResolveLookupGuid(lookupVal, numberToGuid?)GUID из lookup-значения (itemId / item_id)
ReadLookupGuid(item, fieldId, numberToGuid?)Lookup GUID из field_values
ParseDateTimeLocal(raw)Datetime из ISO/UTC в локальное время процесса
ReadDateTimeLocal(item, fieldId)Datetime поля из field_values в локальном времени

In-memory кэш ответов внешних API (общий на процесс backend):

ЧленОписание
DefaultTtlTTL по умолчанию (1 час)
GetOrCreateAsync(key, factory, ct, ttl?, shouldCache?)Вернуть из кэша или вычислить и сохранить
var data = await WebPartResponseCache.GetOrCreateAsync(
"ext:weather:" + city,
ct => FetchWeatherAsync(city, ct),
ct,
ttl: TimeSpan.FromMinutes(15));
Окно терминала
# Скачайте portal-sdk-X.Y.Z.zip с https://korport.ru/developers
dotnet nuget add source https://korport.ru/api/nuget/v3/index.json -n korport
dotnet new install ./portal-sdk-1.0.0/templates/portal-webpart
dotnet new portal-webpart -n MyWidget -o ./my-widget

Пример в SDK ZIP: examples/hello-widget. См. Extension SDK.

Разработка в монорепозитории Portal (вендор)
Окно терминала
cd backend
dotnet new install ./templates/portal-webpart
dotnet new portal-webpart -n MyWidget -o ../../packages/my-widget

См. manifest.md — поле runtime: "dotnet", entry, entryType.