use super::{AppState, Call, CallKind, Failure, asset};
use crate::action::Error;
use crate::form::{Reading, read_moment};
use crate::i18n::Text;
use crate::render::Render;
use crate::time::{DateWords, range_presets};
use crate::ui::{NodePath, input_text, picker_config};
use crate::value::{FieldKind, Value};
use axum::Router;
use axum::extract::State;
use axum::http::HeaderMap;
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use serde::Deserialize;
use serde_json::Value as Json;
use serde_json::json;
use std::sync::Arc;
const JS: &str = include_str!("../../assets/components.js");
const CSS: &str = include_str!("../../assets/components.css");
const CATALOG_JS: &str = include_str!("../../assets/catalog.js");
const CATALOG_CSS: &str = include_str!("../../assets/catalog.css");
const CONTENT_CSS: &str = include_str!("../../assets/content.css");
pub(super) const ASSETS: [&str; 5] = [
"/_zeyn/components.js",
"/_zeyn/components.css",
"/_zeyn/catalog.js",
"/_zeyn/catalog.css",
"/_zeyn/content.css",
];
pub(super) fn head(base: &str) -> String {
let v = super::ops::asset_version();
format!(
r#"<link rel="stylesheet" href="{base}/_zeyn/components.css{v}"><script type="module" src="{base}/_zeyn/components.js{v}"></script><link rel="stylesheet" href="{base}/_zeyn/catalog.css{v}"><link rel="stylesheet" href="{base}/_zeyn/content.css{v}">"#
)
}
pub(super) fn routes(router: Router<Arc<AppState>>) -> Router<Arc<AppState>> {
router
.route(
"/_zeyn/components.js",
get(|| async { asset(JS, "text/javascript; charset=utf-8") }),
)
.route(
"/_zeyn/components.css",
get(|| async { asset(CSS, "text/css; charset=utf-8") }),
)
.route(
"/_zeyn/catalog.js",
get(|| async { asset(CATALOG_JS, "text/javascript; charset=utf-8") }),
)
.route(
"/_zeyn/catalog.css",
get(|| async { asset(CATALOG_CSS, "text/css; charset=utf-8") }),
)
.route(
"/_zeyn/content.css",
get(|| async { asset(CONTENT_CSS, "text/css; charset=utf-8") }),
)
.route("/_zeyn/node", post(node_call))
.route("/_zeyn/data", post(data_call))
.route("/_zeyn/parse", post(parse_call))
}
#[derive(Deserialize)]
struct ParseCall {
kind: String,
#[serde(default)]
text: String,
#[serde(default)]
label: String,
}
async fn parse_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<ParseCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
let words = DateWords::from_i18n(ctx.i18n(), ctx.locale());
let reading = Reading::new(ctx.locale(), ctx.time_zone(), ctx.now(), &words);
let r = Render::new("parse", ctx.locale(), ctx.i18n()).with_tz(ctx.time_zone());
if call.kind == "presets" {
let today = ctx.today();
let presets: Vec<Json> = range_presets(today)
.into_iter()
.map(|(key, from, to)| {
json!({ "key": key, "label": ctx.t(&Text::key(format!("zeyn.ui.date.presets.{key}"))), "from": from.to_string(), "to": to.to_string() })
})
.collect();
let mut config = picker_config(&r);
config["today"] = json!(today.to_string());
config["presets"] = json!(presets);
return axum::Json(config).into_response();
}
let kind = match call.kind.as_str() {
"date" => FieldKind::Date,
"time" => FieldKind::Time,
"datetime" => FieldKind::DateTime,
"local" => FieldKind::LocalDateTime,
_ => return st.error_response(&ctx, Failure::App(Error::NotFound)),
};
let label: String = call.label.chars().take(200).collect();
let text: String = call.text.chars().take(100).collect();
if text.trim().is_empty() {
return axum::Json(json!({ "ok": true, "text": "", "iso": "" })).into_response();
}
match read_moment(kind, &text, &reading) {
Ok(value) => {
let iso = match &value {
Value::DateTime(d) => d.local(ctx.time_zone()).to_string(),
other => serde_json::to_value(other)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default(),
};
let utc = match &value {
Value::DateTime(d) => Some(d.to_string()),
_ => None,
};
axum::Json(json!({ "ok": true, "text": input_text(&value, &r), "iso": iso, "utc": utc, "zone": ctx.time_zone().name() }))
.into_response()
}
Err(skipped) => {
let key = if skipped {
"zeyn.validation.skipped_time"
} else {
"zeyn.ui.date.invalid"
};
axum::Json(
json!({ "ok": false, "message": ctx.t(&Text::key(key).arg("label", label)) }),
)
.into_response()
}
}
}
#[derive(Deserialize)]
struct NodeCall {
screen: String,
#[serde(default)]
params: Json,
node: NodePath,
}
#[derive(Deserialize)]
struct DataCall {
screen: String,
#[serde(default)]
params: Json,
node: NodePath,
#[serde(default)]
key: String,
#[serde(default)]
input: Json,
}
async fn node_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<NodeCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
let Some((&name, _)) = st
.screens
.get_key_value(call.screen.as_str())
else {
return st.error_response(&ctx, Failure::App(Error::NotFound));
};
let info = Call {
kind: CallKind::Screen,
screen: name,
action: None,
via_agent: ctx.via_agent(),
};
let result = st
.intercept(&ctx, &info, || async {
let (_, entry, screen) = st
.open(&ctx, name, call.params.clone())
.await?;
let node = screen
.node(&call.node)
.ok_or(Failure::App(Error::NotFound))?;
super::data::note_screen_read(&ctx, &entry.meta, &call.params);
let loaded = st
.preload(&ctx, screen.walk_from(&call.node), entry.meta.name)
.await;
let r = super::user_render(entry.meta.name, &ctx);
let r = Render {
loaded: Some(&loaded),
..r
};
let mut out = String::new();
crate::ui::render_node(&mut out, &r, call.node.as_str(), node);
Ok::<_, Failure>(out)
})
.await;
match result {
Ok(html) => Html(html).into_response(),
Err(f) => st.error_response(&ctx, f),
}
}
async fn site_data(
st: &AppState,
ctx: &crate::ctx::Ctx,
feature: &str,
call: &DataCall,
) -> Response {
let result = async {
let f = st
.features
.iter()
.find(|f| f.name() == feature)
.ok_or(Failure::App(Error::NotFound))?;
f.check_guards(ctx)
.map_err(Failure::App)?;
let source = f
.site_shell()
.and_then(|s| s.header.node(&call.node))
.and_then(|n| n.source(&call.key))
.ok_or(Failure::App(Error::NotFound))?;
let action = source.action();
if !ctx.identity().can(&action.access) {
return Err(Failure::App(Error::Forbidden));
}
let info = Call {
kind: CallKind::DataGrid,
screen: f.name(),
action: Some(action),
via_agent: ctx.via_agent(),
};
st.intercept(ctx, &info, || {
st.timed(source.fetch(ctx.clone(), call.input.clone()))
})
.await
}
.await;
match result {
Ok(json) => axum::Json(json).into_response(),
Err(f) => st.error_response(ctx, f),
}
}
async fn data_call(
State(st): State<Arc<AppState>>,
headers: HeaderMap,
axum::Json(call): axum::Json<DataCall>,
) -> Response {
let ctx = st.licensed(st.ctx(&headers)).await;
if let Err(f) = st.check_csrf(&headers) {
return st.error_response(&ctx, f);
}
if let Some(feature) = call.screen.strip_prefix("_site.") {
return site_data(&st, &ctx, feature, &call).await;
}
let result = async {
let (_, entry, screen) = st
.open(&ctx, &call.screen, call.params)
.await?;
let source = screen
.node(&call.node)
.and_then(|n| n.source(&call.key))
.ok_or(Failure::App(Error::NotFound))?;
let action = source.action();
if !ctx.identity().can(&action.access) {
return Err(Failure::App(Error::Forbidden));
}
let info = Call {
kind: CallKind::DataGrid,
screen: entry.meta.name,
action: Some(action),
via_agent: ctx.via_agent(),
};
st.intercept(&ctx, &info, || {
st.timed(source.fetch(ctx.clone(), call.input))
})
.await
}
.await;
match result {
Ok(json) => axum::Json(json).into_response(),
Err(f) => st.error_response(&ctx, f),
}
}