Эта страница переведена с английского. Читать страницу на английском
Screen (экран) это страница с маршрутом или окно, открываемое поверх другого экрана. #[screen] объявляет доступ так же, как action.
Страница
#[screen(route = "/products", permission = "stock.product.read")]
pub fn product_list() -> Screen {
Screen::new(t!("stock.products"))
.toolbar(|t| t.button(t!("stock.new_product")).icon(Icon::Plus).shortcut("Alt+N").opens(product_editor, ProductParams { id: None }))
.body(DataGrid::of(search_products).column(Product::code).column(Product::name).flex(1))
}Screen::new(title)это страница;Screen::window(title)это окно.- Несколько вызовов
.body(...)размещают несколько компонентов один под другим. - Экран возвращает
ScreenилиResult<Screen>.
Params
Первый аргумент экрана это его params (параметры): структура #[params] или _: (), если параметров нет. Остальные аргументы внедряются, как в action.
#[params]
pub struct ProductParams {
pub id: Option<ProductId>,
}
#[screen(permission = "stock.product.write")]
pub fn product_editor(params: ProductParams, store: ProductStore) -> Result<Screen> {
let product = match params.id {
Some(id) => store.get(id)?,
None => Product::default(),
};
Ok(Screen::window(t!("stock.product")).width(640).body(Form::of(product).field(Product::code).submit(t!("stock.save"), save_product)))
}Params типизированы везде, где используются: Window::open(product_editor, 7) не компилируется, потому что редактор принимает ProductParams.
У экрана без route нет URL. Он открывается как окно по ссылке, кнопке панели инструментов, из меню или командой.
Params в пути
Маршрут может называть поля params как сегменты пути. Каждый {name} привязывается к полю с тем же именем и читается как тип этого поля:
#[params]
pub struct OrderParams {
pub id: OrderId,
#[serde(default)]
pub tab: Option<String>,
}
#[screen(route = "/orders/{id}", permission = "sales.order.read")]
pub fn order(p: OrderParams, store: OrderStore) -> Result<Screen> {
let order = store.get(p.id).ok_or(Error::NotFound)?;
Ok(Screen::new(t!("sales.order")).body(DescriptionList::new().item(t!("sales.customer"), order.customer)))
}/orders/42открывает заказ 42. Поля, которых нет в пути, передаются как раньше в JSON-запросе:/orders/42?params={"tab":"lines"}.- Сегменты принимают целые числа, ключи entity (
OrderId),StringиSlug. Значение разбирается только в той форме, в которой его пишет фреймворк (7, а не007или+7); всё остальное даёт ответ 404. - Сегмент, не называющий ни одно поле params, маршрут с сегментами у экрана без params или тип поля, который не может быть сегментом, не компилируются, и ошибка говорит, что нужно изменить.
- Несколько сегментов работают так же:
#[screen(route = "/catalog/{slug}/{n}", public)]сslug: Slugиn: u32. - Ссылки, которые пишет фреймворк, используют форму с путём:
Effect::navigate, навигационная цепочка (breadcrumbs), команды иreturn_toпосле входа. Экраны без сегментов сохраняют?params={json}. - Экран с сегментами не может быть пунктом меню или домашним экраном, потому что нечем заполнить сегменты.
- Доступ проверяется при каждом запросе, как для любого экрана.
Slug
Slug::from_title("Çay Bardağı") даёт cay-bardagi: строчный ASCII, слова соединены через -, турецкие буквы заменены (ı и İ на i, ş на s, ğ на g, ç на c, ö на o, ü на u), диакритические знаки других латинских букв отбрасываются, всё остальное опускается. Slug занимает не более 80 байт. В пути разбирается только канонический вид, поэтому /catalog/Cay-Bardagi/1 даёт ответ 404.
Не найдено и перенесено
Верните Err(Error::NotFound), когда записи нет; пользователь получит страницу 404. Когда адрес изменился, например после переименования товара, ответьте новым адресом. Браузер получит постоянное перенаправление (301), а при навигации внутри приложения следует за ним без загрузки страницы:
#[screen(route = "/catalog/{slug}/{n}", public)]
pub fn catalog_page(p: CatalogParams, store: Catalog) -> Result<Screen> {
let item = store.find(p.n).ok_or(Error::NotFound)?;
if item.slug != p.slug {
return Ok(Screen::moved_to(catalog_page, CatalogParams { slug: item.slug, n: p.n }));
}
Ok(Screen::new(item.name))
}Доступ
| Параметр | Значение |
|---|---|
permission = "..." | Субъекту нужно это разрешение |
public | Любой. Посетители без сессии получают страницу без cookie (см. Public pages) |
cache = 300 | Только для публичных экранов: браузеры и общие кэши могут хранить страницу посетителя без сессии указанное число секунд (не более 86400) |
sensitive | Открытие записывается как чтение персональных данных |
reason | Пользователь должен указать причину, прежде чем экран откроется (см. Audit, masking and jobs) |
Страница также сообщает поисковым системам и предпросмотрам ссылок о себе через .description(..), .canonical(..), .og_image(..), .json_ld(..) и .robots(..) у Screen; см. Public pages.
Пункты меню, кнопки панели инструментов и команды, открывающие экран, который пользователю открывать нельзя, исключаются из HTML.
Экран для посетителей, не вошедших в систему, объявляется как public. Его action нужен собственный доступ: публичный экран со списком данных вызывает public action, иначе список для посетителей останется пустым.
#[action(public)]
pub async fn latest_products(q: DataQuery, store: ProductStore) -> Result<Page<Product>> {
Ok(q.apply(store.all()?))
}
#[screen(route = "/catalog", public)]
pub fn catalog() -> Screen {
Screen::new(t!("catalog.title")).body(DataGrid::of(latest_products).column(Product::code).column(Product::price))
}Если опустить и permission, и public, возникает ошибка компиляции, поэтому экран никогда не окажется открытым случайно.
Панель инструментов и контекстное меню
Screen::new(t!("stock.products"))
.toolbar(|t| t.button(t!("stock.new_product")).icon(Icon::Plus).opens(product_editor, ProductParams { id: None }).export(Export::Csv))
.context_menu(ContextMenu::new().item(t!("stock.new_product"), Window::open(product_editor, ProductParams { id: None })).action(t!("stock.restock_low"), restock_low, ()))Сочетания клавиш вроде Alt+N работают на экране и показываются в палитре. Сочетания, которые использует браузер (Ctrl+P, Ctrl+S, F5), не привязываются, если кнопка не вызывает .override_browser(), а те, которые браузер никогда не передаёт странице (Ctrl+N, Ctrl+T, Ctrl+W), не привязываются никогда; свободные комбинации: Alt+Shift+<letter> и Ctrl+Alt+<letter>.
Feature и меню
Экран принадлежит Feature. Префикс маршрута Feature ставится перед маршрутом, её guard выполняются до открытия экрана, а её меню перечисляет экраны:
Feature::new("stock")
.route_prefix("/stock")
.guard(SignedIn)
.screen(product_list)
.screen(product_editor)
.menu(|m| m.group(t!("menu.stock"), Icon::Box).item(t!("stock.products"), product_list))App::home(screen) делает так, что / открывает экран.
Страницы ошибок
App::not_found(screen) и App::server_error(screen) дают приложению собственные страницы для 404 и для ошибок сервера:
#[screen(public)]
pub fn not_found_page() -> Screen {
Screen::new(t!("errors.not_found")).body(EmptyState::new(t!("errors.not_found_text")))
}
App::new(t!("app.name")).not_found(not_found_page).server_error(server_error_page)- Экран 404 отвечает на неизвестные пути и на каждый экран, возвращающий
Err(Error::NotFound), включая значения пути, которые не разбираются. - Экран ошибки сервера отвечает на
Error::Internal, тайм-ауты (504) и паники в обработчиках. Подробности ошибки попадают только в журнал сервера; экран не получает params и ничего не узнаёт о запросе. - Оба показываются внутри оболочки (shell) с правильным кодом статуса. При навигации внутри приложения приходит только экран, и он подставляется без загрузки страницы. Конечные точки фреймворка и API продолжают отвечать JSON.
- Экраны ошибок не принимают params, не требуют маршрута и не входят в Feature. Делайте их
public: пользователь, которому нельзя открыть такой экран, или экран ошибки, который сам даёт сбой, получит короткое встроенное сообщение.
Как экран попадает в браузер
Первый запрос получает полную страницу: меню, экран и ресурсы. Ссылки внутри приложения затем загружают только экран и подставляют его на место, поэтому меню, состояние открытых элементов и положение прокрутки сохраняются. Кнопка «назад», новые вкладки и закладки работают как с обычными ссылками.