Роутинг¶
В платформе существуют два основных типа роутинга: для карточек /card/:id
и для рабочих мест /view/:id
. Платформа позволяет переопределять существующий ройтинг и задавать новый.
Создание нового роута¶
Для примера создадим роут /custom_route
, при переходе на который пользователь должен увидеть название роута в центре экрана.
Для начала нужно описать новый контроллер со стороны сервера:
namespace Tessa.Extensions.Server.Web.Services
{
[AllowAnonymous]
public sealed class CustomRouteController :
Controller
{
#region Constructors
public CustomRouteController(IClientViewProvider viewProvider)
{
this.viewProvider = NotNullOrThrow(viewProvider);
}
#endregion
#region Fields
private readonly IClientViewProvider viewProvider;
#endregion
#region Controller Methods
[Route("custom_route"), SessionMethod]
public Task<IActionResult> GetCustomRoute(CancellationToken cancellationToken = default)
{
// при переходе по маршруту просто отдаем базовую html страницу со всеми скриптами
return this.viewProvider.GetIndexViewAsync(this, cancellationToken: cancellationToken);
}
#endregion
}
}
Для возможности редиректа на окно логина нужно добавить следующий код:
namespace Tessa.Extensions.Server.Web.Services
{
public class CustomRoutePathParser : ClientPathParser
{
[WebRegistrator]
public sealed class WebRegistrator : WebRegistratorBase
{
public override void RegisterServices() =>
this.Services.AddSingleton<IClientPathParser, CustomRoutePathParser>();
}
public CustomRoutePathParser(IWebPathParser webPathParser)
: base(webPathParser)
{
this.RedirectableToLoginPrefixes.Add("/custom_route");
}
}
}
Note
Для простоты класс регистратора WebRegistrator
добавлен внутрь класса CustomRoutePathParser
, т.к. он регистрирует единственную зависимость. Его возможно указать и как обычный класс в пространстве имён (по аналогии с классами Registrator
).
Со стороны клиента нужно создать новый тип вкладки, связанный с роутом:
export class CustomWorkspace extends DefaultWorkspaceModelBase {
//#region static
// вкладка всегда будет в одном экземпляре, поэтому заранее создаем для неё id
readonly WorkspaceId = Guid.newGuid();
//#endregion
//#region ctor
constructor() {
super(CustomWorkspace.WorkspaceId);
}
//#endregion
//#region props
override get workspaceName(): string {
return 'Custom';
}
//#endregion
//#region methods
override getRoute(): RouteData {
// путь, который устаналивается при переходе на вкладку
return { path: '/custom_route' };
}
//#endregion
}
Далее опишем компонент, который будет отображаться на экране при переходе по роуту:
const Container = styled.div`
display: flex;
justify-content: center;
align-content: center;
font-size: 80px;
`;
export const CustomRouteComponent: RouteComponent = ({ route }) => {
// из компонента можно получить доступ до объекта роута
console.log(route);
return (
<div className="dashboard-wrapper">
<Container>Custom Route</Container>
</div>
);
};
Далее нужно создать объект с описанием нового роута, который свяжет вместе все ранее созданные элементы, и зарегистрировать его в платформе:
// customRoute.ts
@injectable()
export class CustomRoute extends Route {
override get path(): string {
return '/custom_route';
}
override get component(): RouteComponent {
return CustomRouteComponent;
}
override async transit(): Promise<void> {
// если вкладка ранее уже была открыта, то просто переходим на неё. в противном случае создаем вкладку.
const workspace = this.workspaceStorage.tryGetWorkspace(CustomWorkspace.WorkspaceId);
if (!workspace) {
this.workspaceStorage.addWorkspace(new CustomWorkspace());
}
await this.workspaceStorage.activateWorkspace(CustomWorkspace.WorkspaceId, false);
}
}
// registrator.ts
export const SolutionRegistrator: ExtensionRegistrator = {
async registerTypes(container) {
container.bind(IRoute$).to(CustomRoute).inSingletonScope();
}
};
Чтобы перейти на новый роут, можно:
- добавить в адресную строку браузера
https://%server_name%/custom_route
; - из кода вызвать
Application.instance.history.push('/custom_route')
.
Переопределение существующего маршурта¶
Для примера переопределим роут для карточек “Автомобиль”, чтобы была возможность открывать карточки этого типа по роуту /car/:car_name
.
Для начала нужно описать новый контроллер со стороны сервера:
namespace Tessa.Extensions.Server.Web.Services
{
[Route("car"), AllowAnonymous]
public sealed class CarController :
Controller
{
#region Constructors
public CarController(IClientViewProvider viewProvider)
{
this.viewProvider = NotNullOrThrow(viewProvider);
}
#endregion
#region Fields
private readonly IClientViewProvider viewProvider;
#endregion
#region Controller Methods
[HttpGet("{name}"), SessionMethod]
public Task<IActionResult> GetCar([FromRoute] string name, CancellationToken cancellationToken = default)
{
// получаем ID карточки по названию автомобиля, указанного в карточке
// в дальнейшем этот ID можно будет использовать при переходе
var cardID = await this.GetCarCardIDByName(name);
return this.viewProvider.GetIndexViewAsync(
this,
async () =>
{
var storage = new Dictionary<string, object?>();
storage["CardID"] = cardID;
return storage;
},
cancellationToken);
}
[HttpGet("api/car/{name}"), SessionMethod, Produces(MediaTypeNames.Text.Plain)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<string> GetCard(
[FromRoute] string name,
CancellationToken cancellationToken = default)
{
var cardID = await this.GetCarCardIDByName(name);
return cardID.ToString();
}
private async Task<Guid> GetCarCardIDByName(string name)
{
// получаем нужную карточку. например через DbScope
// ...
}
#endregion
}
}
Для возможности редиректа на окно логина нужно добавить следующий код:
namespace Tessa.Extensions.Server.Web.Services
{
public class CarRoutePathParser : ClientPathParser
{
[WebRegistrator]
public sealed class WebRegistrator : WebRegistratorBase
{
public override void RegisterServices() =>
this.Services.AddSingleton<IClientPathParser, CarRoutePathParser>();
}
public CarRoutePathParser(IWebPathParser webPathParser)
: base(webPathParser)
{
this.RedirectableToLoginPrefixes.Add("/car/");
}
}
}
Note
Для простоты класс регистратора WebRegistrator
добавлен внутрь класса CarRoutePathParser
, т.к. он регистрирует единственную зависимость. Его возможно указать и как обычный класс в пространстве имён (по аналогии с классами Registrator
).
Нужно создать объект с описанием нового роута для карточки и зарегистрировать его:
@injectable()
export class CarRoute extends CardRoute {
constructor(@inject(IApiClient$) private readonly _apiClient: IApiClient) {
super();
}
override get path(): string {
// через `:` можно указывать параметры в роута, которые будут доступны для получения через `.getParams()`
return '/car/:name';
}
override async canTransit(opt: RouteTransitionOptions): Promise<boolean> {
const { name } = opt.getParams<{ name: string }>();
return name != null;
}
override async transit(opt: RouteTransitionOptions): Promise<void> {
// достаем название автомобиля из параметров роута
const { name } = opt.getParams<{ name: string }>();
// актуальный ID карточки может быть получен заранее и добавлен в стейт роута
let cardId = opt.location.state?.cardId;
// если в стейте пусто и это стартовый роут (был открыт через адресную строку браузера), то
// пытаемся получить ID карточки через глобальные данные роута, которые были заданы на сервере в CarController.GetCar
if (!cardId && opt.initial) {
cardId = opt.getGlobalRouteData()?.['CardID'];
}
// если ID найти не удалось, то для открытия карточки получаем правильный ID с сервера
if (!cardId) {
cardId = await this._apiClient.get(`/car/api/card/${encodeURIComponent(name)}`).text();
}
// открываем карточку как обычно
await openCard({
cardId,
needDispatch: false
});
}
}
// registrator.ts
export const SolutionRegistrator: ExtensionRegistrator = {
async registerTypes(container) {
container.bind(IRoute$).to(CarRoute).inSingletonScope();
}
};
Для того, чтобы в адресной строке был новый адрес при открытии карточек автомобиля из приложения, нужно добавить UI расширение:
@extension({ name: 'CarRouteUIExtension' })
export class CarRouteUIExtension extends CardUIExtension {
override initializing(context: ICardUIExtensionContext): void {
const { card, uiContext } = context;
const cardName = card.sections.tryGet('TEST_CarMainInfo')?.fields.tryGet('Name');
if (cardName && uiContext.cardEditor) {
uiContext.cardEditor.routeOverride = () => ({
path: `/car/${encodeURIComponent(cardName)}`,
state: {
cardId: card.id // сразу сохраняем id карточки, чтобы не приходилось вызывать лишний раз сервер при переходах
}
});
}
}
}
// registrator.ts
export const SolutionRegistrator: ExtensionRegistrator = {
async registerExtensions(container) {
container.registerExtension({
extension: CarRouteUIExtension,
stage: ExtensionStage.AfterPlatform,
when: whenCardTypeIdIs('d0006e40-a342-4797-8d77-6501c4b7c4ac')
});
}
};
Каждый раз при открытии/обновлении карточки дефолтный путь card/:id
будет заменяться на car/:car_name
.