use super::{AppState, Call, CallKind, Failure, asset};
use crate::action::Error;
use crate::ctx::Ctx;
use crate::i18n::Text;
use crate::render::{Render, escape};
use axum::Router;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use serde::Deserialize;
use serde_json::json;
use std::sync::Arc;
const JS: &str = include_str!("../../assets/pro.js");
const CSS: &str = include_str!("../../assets/pro.css");
pub(super) const ASSETS: [&str; 2] = ["/_zeyn/pro.js", "/_zeyn/pro.css"];
pub(super) fn head(base: &str) -> String {
format!(
r#"<link rel="stylesheet" href="{base}/_zeyn/pro.css"><script type="module" src="{base}/_zeyn/pro.js"></script>"#
)
}
pub(super) fn routes(router: Router<Arc<AppState>>) -> Router<Arc<AppState>> {
router
.route("/_zeyn/pro.js", get(|| async { asset(JS, "text/javascript; charset=utf-8") }))
.route("/_zeyn/pro.css", get(|| async { asset(CSS, "text/css; charset=utf-8") }))
.route("/_zeyn/command", post(command_call))
.route("/_zeyn/dock", post(dock_call))
.route("/_zeyn/search", post(search_call))
}
#[derive(Deserialize)]
struct SearchCall {
query: String,
}
async fn search_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<SearchCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
let query: String = call.query.trim().chars().take(crate::command::MAX_SEARCH_QUERY).collect();
let mut hits = Vec::new();
if query.is_empty() {
return axum::Json(json!({ "hits": hits })).into_response();
}
for f in &st.features {
if f.check_guards(&ctx).is_err() {
continue;
}
for search in f.searches() {
let meta = *search.meta();
if !ctx.identity().can(&meta.access) {
continue;
}
let info =
Call { kind: CallKind::Screen, screen: f.name(), action: Some(meta), via_agent: ctx.via_agent() };
let result = st.intercept(&ctx, &info, || st.timed(search.run(ctx.clone(), query.clone()))).await;
match result {
Ok(found) => hits.extend(found.into_iter().filter(crate::command::SearchHit::is_local)),
Err(_) => eprintln!("warning: palette search `{}` failed", meta.name),
}
}
}
hits.truncate(crate::command::MAX_SEARCH_HITS);
axum::Json(json!({ "hits": hits })).into_response()
}
#[derive(Deserialize)]
struct DockCall {
screen: String,
#[serde(default)]
params: serde_json::Value,
node: crate::ui::NodePath,
#[serde(default)]
save: bool,
#[serde(default)]
layout: Option<crate::ui::DockLayout>,
}
async fn dock_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<DockCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
let result = async {
let store = st.views.as_ref().ok_or(Failure::App(Error::NotFound))?;
let user = ctx.identity().user.clone().ok_or(Failure::App(Error::Forbidden))?;
let (_, entry, screen) = st.open(&ctx, &call.screen, call.params).await?;
let node = screen.node(&call.node).ok_or(Failure::App(Error::NotFound))?;
let known = crate::ui::dock_panel_ids(&node.schema()).ok_or(Failure::App(Error::NotFound))?;
let known: Vec<&str> = known.iter().map(String::as_str).collect();
let key = format!("{}~dock", AppState::view_key(&ctx, entry.meta.name, &call.node));
if call.save {
let view = call.layout.map(|l| l.clean(&known).to_view());
st.timed(store.save(&user, &key, view)).await?;
return Ok(None);
}
let view = st.timed(store.load(&user, &key)).await?;
let layout = view.as_ref().and_then(crate::ui::DockLayout::from_view).map(|l| l.clean(&known));
Ok(Some(json!({ "layout": layout })))
}
.await;
match result {
Ok(Some(body)) => axum::Json(body).into_response(),
Ok(None) => axum::http::StatusCode::NO_CONTENT.into_response(),
Err(f) => st.error_response(&ctx, f),
}
}
pub(super) fn shell_parts(st: &AppState, ctx: &Ctx, r: &Render) -> (String, String) {
let t = |key: &str| r.text(&Text::key(format!("zeyn.ui.palette.{key}")));
let keys = escape(&st.palette_keys);
let button = format!(
r#"<button type="button" class="z-palette-open" data-zeyn-palette-open aria-haspopup="dialog" aria-keyshortcuts="{keys}"><span class="z-icon z-icon-search" aria-hidden="true"></span><span>{}</span><kbd aria-hidden="true">{}</kbd></button>"#,
t("open"),
escape(&st.palette_keys.replace("Control", "Ctrl"))
);
let search = st
.features
.iter()
.any(|f| f.check_guards(ctx).is_ok() && f.searches().iter().any(|s| ctx.identity().can(&s.meta().access)));
let mut commands = format!(
r#"<div class="z-commands" hidden data-zeyn-commands{} data-palette-keys="{keys}" data-text-label="{}" data-text-placeholder="{}" data-text-empty="{}" data-text-count="{}" data-text-recent="{}" data-text-app="{}" data-text-screen="{}" data-text-go="{}" data-text-done="{}" data-text-hint="{}">"#,
if search { " data-search" } else { "" },
t("label"),
t("placeholder"),
t("empty"),
t("count"),
t("recent"),
t("app"),
t("screen"),
t("go"),
t("done"),
t("hint"),
);
for f in &st.features {
if f.check_guards(ctx).is_err() {
continue;
}
for c in f.commands() {
if ctx.identity().can(&c.access()) {
commands.push_str(&c.button(r));
}
}
}
commands.push_str("</div>");
(button, commands)
}
#[derive(Deserialize)]
struct CommandCall {
command: String,
}
async fn command_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<CommandCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
let result = async {
let (feature, command) = st
.features
.iter()
.find_map(|f| f.commands().iter().find(|c| c.id() == call.command).map(|c| (f, c)))
.ok_or(Failure::App(Error::NotFound))?;
feature.check_guards(&ctx)?;
if !ctx.identity().can(&command.access()) {
return Err(Failure::App(Error::Forbidden));
}
let run = command.call().ok_or(Failure::App(Error::NotFound))?;
let info =
Call { kind: CallKind::Screen, screen: command.id(), action: Some(run.meta), via_agent: ctx.via_agent() };
st.intercept(&ctx, &info, || st.timed((run.run)(ctx.clone()))).await
}
.await;
match result {
Ok(()) => axum::Json(json!({ "effects": [] })).into_response(),
Err(f) => st.error_response(&ctx, f),
}
}