we getting there...
This commit is contained in:
parent
9011c9f34d
commit
305a58e8b5
45 changed files with 760 additions and 85 deletions
12
app-template-ui/.neoconf.json
Normal file
12
app-template-ui/.neoconf.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"lspconfig": {
|
||||
"rust_analyzer": {
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.rustfmt.overrideCommand": [
|
||||
"leptosfmt",
|
||||
"--stdin",
|
||||
"--rustfmt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ crate-type = ["cdylib", "rlib"]
|
|||
axum = { workspace = true, optional = true }
|
||||
console_error_panic_hook = { workspace = true }
|
||||
leptos = { workspace = true, features = ["nightly"] }
|
||||
anyhow = { workspace = true, optional = true }
|
||||
leptos_axum = { workspace = true, optional = true }
|
||||
leptos_meta = { workspace = true, features = ["nightly"] }
|
||||
leptos_router = { workspace = true, features = ["nightly"] }
|
||||
|
@ -29,13 +30,20 @@ tower = { workspace = true, optional = true }
|
|||
tower-http = { workspace = true, features = ["fs"], optional = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
thiserror = { workspace = true }
|
||||
http = { workspace = true }
|
||||
pulldown-cmark = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
saleor-app-sdk = { workspace = true }
|
||||
saleor-app-sdk = { workspace = true, features = ["bridge"], optional = true }
|
||||
dotenvy = { workspace = true }
|
||||
envy = { workspace = true }
|
||||
cynic = { workspace = true, features = ["http-surf"], optional = true }
|
||||
surf = { workspace = true, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
cynic-codegen = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
|
@ -45,12 +53,19 @@ ssr = [
|
|||
"dep:tower",
|
||||
"dep:tower-http",
|
||||
"dep:leptos_axum",
|
||||
"dep:tracing",
|
||||
"dep:saleor-app-sdk",
|
||||
"dep:tracing-subscriber",
|
||||
"dep:anyhow",
|
||||
"dep:cynic",
|
||||
"dep:cynic-codegen",
|
||||
"dep:surf",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"dep:tracing",
|
||||
]
|
||||
|
||||
|
||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
|
|
11
app-template-ui/build.rs
Normal file
11
app-template-ui/build.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
fn main() {
|
||||
cynic_codegen::register_schema("saleor")
|
||||
.from_sdl_file("../schema.graphql")
|
||||
.unwrap()
|
||||
.as_default()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
fn main() {}
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 264 KiB |
BIN
app-template-ui/public/fonts/Inter-Black.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Black.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-BlackItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-BlackItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Bold.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-BoldItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-ExtraBold.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-ExtraLight.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-ExtraLightItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Italic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Light.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Light.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-LightItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-LightItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Medium.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-MediumItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Regular.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-SemiBold.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-Thin.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-Thin.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-ThinItalic.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-italic.var.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-italic.var.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter-roman.var.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter-roman.var.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/fonts/Inter.var.woff2
Normal file
BIN
app-template-ui/public/fonts/Inter.var.woff2
Normal file
Binary file not shown.
BIN
app-template-ui/public/logo.png
Normal file
BIN
app-template-ui/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
5
app-template-ui/rust-toolchain.toml
Normal file
5
app-template-ui/rust-toolchain.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2024-04-28"
|
||||
## Toggle to this one for sdk releases
|
||||
# channel = "stable"
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
|
@ -15,10 +15,10 @@ pub fn App() -> impl IntoView {
|
|||
provide_meta_context();
|
||||
|
||||
view! {
|
||||
<Stylesheet id="leptos" href="/pkg/saleor-marketplace.css"/>
|
||||
<Stylesheet id="leptos" href="/pkg/saleor-app-template-ui.css"/>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Saleors Harbour"/>
|
||||
<Title text="Example UI App template in Rust"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router fallback=|| {
|
||||
|
@ -28,9 +28,18 @@ pub fn App() -> impl IntoView {
|
|||
}>
|
||||
<main class="p-4 md:p-8 md:px-16">
|
||||
<Routes>
|
||||
<Route path="" view=Home/>
|
||||
<Route path="/" view=Home/>
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppState {
|
||||
pub saleor_app: std::sync::Arc<tokio::sync::Mutex<saleor_app_sdk::SaleorApp>>,
|
||||
pub config: saleor_app_sdk::config::Config,
|
||||
pub manifest: saleor_app_sdk::manifest::AppManifest,
|
||||
pub leptos_options: LeptosOptions,
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
use leptos::*;
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub mod app_editor;
|
||||
|
||||
|
|
|
@ -1,7 +1,41 @@
|
|||
use http::status::StatusCode;
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use axum::response::{IntoResponse, Response};
|
||||
#[cfg(feature = "ssr")]
|
||||
use http::header::ToStrError;
|
||||
use http::status::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
/* ERROR STUFF FOR AXUM */
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AxumError {
|
||||
#[error("Error converting something to string, `{0}`")]
|
||||
ToStrError(#[from] ToStrError),
|
||||
#[error("Anyhow function error: {0}")]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
#[error("Request is missing header `{0}`")]
|
||||
MissingHeader(String),
|
||||
#[error("Internal server error, `{0}`")]
|
||||
InternalServerError(String),
|
||||
}
|
||||
|
||||
// Tell axum how to convert `AppError` into a response.
|
||||
#[cfg(feature = "ssr")]
|
||||
impl IntoResponse for AxumError {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {:?}", self),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/* THIS IS ERROR STUFF FOR LEPTOS */
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
|
@ -52,16 +86,16 @@ pub fn ErrorTemplate(
|
|||
}
|
||||
|
||||
view! {
|
||||
<h1>{if errors.len() > 1 {"Errors"} else {"Error"}}</h1>
|
||||
<h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1>
|
||||
<For
|
||||
// a function that returns the items we're iterating over; a signal is fine
|
||||
each= move || {errors.clone().into_iter().enumerate()}
|
||||
each=move || { errors.clone().into_iter().enumerate() }
|
||||
// a unique key for each item as a reference
|
||||
key=|(index, _error)| *index
|
||||
// renders each item to a view
|
||||
children=move |error| {
|
||||
let error_string = error.1.to_string();
|
||||
let error_code= error.1.status_code();
|
||||
let error_code = error.1.status_code();
|
||||
view! {
|
||||
<h2>{error_code.to_string()}</h2>
|
||||
<p>"Error: " {error_string}</p>
|
||||
|
|
|
@ -2,10 +2,11 @@ pub mod app;
|
|||
pub mod error_template;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fileserv;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod queries;
|
||||
|
||||
pub mod components;
|
||||
pub mod routes;
|
||||
pub mod server;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
|
|
@ -1,44 +1,133 @@
|
|||
pub mod server;
|
||||
pub mod components;
|
||||
pub mod routes;
|
||||
#![allow(
|
||||
non_upper_case_globals,
|
||||
clippy::large_enum_variant,
|
||||
clippy::upper_case_acronyms,
|
||||
dead_code
|
||||
)]
|
||||
#![feature(let_chains)]
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod fileserv;
|
||||
mod fileserv;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod error_template;
|
||||
mod queries;
|
||||
|
||||
mod app;
|
||||
mod components;
|
||||
mod routes;
|
||||
mod error_template;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod app;
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(feature = "ssr")]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
|
||||
use axum::Router;
|
||||
use std::sync::Arc;
|
||||
use axum::{middleware, routing::{post,get}, Router};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use crate::app::*;
|
||||
use crate::fileserv::file_and_error_handler;
|
||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||
// For deployment these variables are:
|
||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||
// The file would need to be included with the executable when moved to deployment
|
||||
use app::*;
|
||||
use fileserv::file_and_error_handler;
|
||||
use saleor_app_sdk::middleware::verify_webhook_signature::webhook_signature_verifier;
|
||||
use tokio::sync::Mutex;
|
||||
use saleor_app_sdk::{
|
||||
cargo_info,
|
||||
config::Config,
|
||||
manifest::{AppManifestBuilder, AppPermission},
|
||||
webhooks::{AsyncWebhookEventType, WebhookManifestBuilder},
|
||||
SaleorApp,
|
||||
};
|
||||
|
||||
use crate::routes::api::{manifest::manifest, register::register, webhooks::webhooks};
|
||||
|
||||
//Leptos stuff
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let leptos_options = conf.leptos_options;
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
// Saleor stuff
|
||||
let config = Config::load().unwrap();
|
||||
trace_to_std(&config).unwrap();
|
||||
let saleor_app = SaleorApp::new(&config).unwrap();
|
||||
|
||||
let app_manifest = AppManifestBuilder::new(&config, cargo_info!())
|
||||
.add_webhook(
|
||||
WebhookManifestBuilder::new(&config)
|
||||
.set_query(
|
||||
r#"
|
||||
subscription QueryProductsChanged {
|
||||
event {
|
||||
... on ProductUpdated {
|
||||
product {
|
||||
... BaseProduct
|
||||
}
|
||||
}
|
||||
... on ProductCreated {
|
||||
product {
|
||||
... BaseProduct
|
||||
}
|
||||
}
|
||||
... on ProductDeleted {
|
||||
product {
|
||||
... BaseProduct
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment BaseProduct on Product {
|
||||
id
|
||||
slug
|
||||
name
|
||||
category {
|
||||
slug
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.add_async_events(vec![
|
||||
AsyncWebhookEventType::ProductCreated,
|
||||
AsyncWebhookEventType::ProductUpdated,
|
||||
AsyncWebhookEventType::ProductDeleted,
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
.add_permission(AppPermission::ManageProducts)
|
||||
.build();
|
||||
|
||||
let app_state = AppState{
|
||||
manifest: app_manifest,
|
||||
config: config.clone(),
|
||||
saleor_app: Arc::new(Mutex::new(saleor_app)),
|
||||
leptos_options,
|
||||
};
|
||||
|
||||
let state_1 = app_state.clone();
|
||||
let app =
|
||||
Router::new()
|
||||
.layer(middleware::from_fn(webhook_signature_verifier))
|
||||
.route("/api/webhooks", post(webhooks))
|
||||
.route("/api/register", post(register))
|
||||
.route("/api/manifest", get(manifest))
|
||||
// .leptos_routes_with_context(&leptos_options, routes,move || provide_context(state_1.clone()) , App)
|
||||
.leptos_routes(&leptos_options, routes, App)
|
||||
.fallback(file_and_error_handler)
|
||||
.with_state(leptos_options);
|
||||
.with_state(app_state.clone());
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
logging::log!("listening on http://{}", &addr);
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(
|
||||
"0.0.0.0:".to_owned()
|
||||
+ config
|
||||
.app_api_base_url
|
||||
.split(':')
|
||||
.collect::<Vec<_>>()
|
||||
.get(2)
|
||||
.unwrap_or(&"3000"),
|
||||
)
|
||||
.await?;
|
||||
tracing::debug!("listening on {}", listener.local_addr()?);
|
||||
|
||||
let _= axum::serve(listener, app.into_make_service())
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
|
@ -47,3 +136,26 @@ pub fn main() {
|
|||
// unless we want this to work with e.g., Trunk for a purely client-side app
|
||||
// see lib.rs for hydration function instead
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use saleor_app_sdk::config::Config;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn trace_to_std(config: &Config) -> Result<(), envy::Error> {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::DEBUG.into())
|
||||
.from_env().unwrap()
|
||||
.add_directive(format!("{}={}", env!("CARGO_PKG_NAME"), config.log_level).parse().unwrap());
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(config.log_level)
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.compact()
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
|
45
app-template-ui/src/queries/event_products_updated.rs
Normal file
45
app-template-ui/src/queries/event_products_updated.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
#[cynic::schema("saleor")]
|
||||
mod schema {}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(graphql_type = "Subscription")]
|
||||
pub struct QueryProductsChanged {
|
||||
pub event: Option<Event>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct ProductUpdated {
|
||||
pub product: Option<Product>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct ProductDeleted {
|
||||
pub product: Option<Product>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct ProductCreated {
|
||||
pub product: Option<Product>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct Product {
|
||||
pub id: cynic::Id,
|
||||
pub slug: String,
|
||||
pub name: String,
|
||||
pub category: Option<Category>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct Category {
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::InlineFragments, Debug)]
|
||||
pub enum Event {
|
||||
ProductUpdated(ProductUpdated),
|
||||
ProductCreated(ProductCreated),
|
||||
ProductDeleted(ProductDeleted),
|
||||
#[cynic(fallback)]
|
||||
Unknown,
|
||||
}
|
2
app-template-ui/src/queries/mod.rs
Normal file
2
app-template-ui/src/queries/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod event_products_updated;
|
||||
pub mod product_metadata_update;
|
75
app-template-ui/src/queries/product_metadata_update.rs
Normal file
75
app-template-ui/src/queries/product_metadata_update.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
#[cynic::schema("saleor")]
|
||||
mod schema {}
|
||||
|
||||
#[derive(cynic::QueryVariables, Debug)]
|
||||
pub struct UpdateProductMetadataVariables<'a> {
|
||||
pub metadata: Option<Vec<MetadataInput<'a>>>,
|
||||
pub product_id: &'a cynic::Id,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
#[cynic(
|
||||
graphql_type = "Mutation",
|
||||
variables = "UpdateProductMetadataVariables"
|
||||
)]
|
||||
pub struct UpdateProductMetadata {
|
||||
#[arguments(id: $product_id, input: { metadata: $metadata })]
|
||||
pub product_update: Option<ProductUpdate>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct ProductUpdate {
|
||||
pub errors: Vec<ProductError>,
|
||||
pub product: Option<Product>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct Product {
|
||||
pub id: cynic::Id,
|
||||
pub metadata: Vec<MetadataItem>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct ProductError {
|
||||
pub field: Option<String>,
|
||||
pub message: Option<String>,
|
||||
pub code: ProductErrorCode,
|
||||
pub attributes: Option<Vec<cynic::Id>>,
|
||||
pub values: Option<Vec<cynic::Id>>,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment, Debug)]
|
||||
pub struct MetadataItem {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(cynic::Enum, Clone, Copy, Debug)]
|
||||
pub enum ProductErrorCode {
|
||||
AlreadyExists,
|
||||
AttributeAlreadyAssigned,
|
||||
AttributeCannotBeAssigned,
|
||||
AttributeVariantsDisabled,
|
||||
MediaAlreadyAssigned,
|
||||
DuplicatedInputItem,
|
||||
GraphqlError,
|
||||
Invalid,
|
||||
InvalidPrice,
|
||||
ProductWithoutCategory,
|
||||
NotProductsImage,
|
||||
NotProductsVariant,
|
||||
NotFound,
|
||||
Required,
|
||||
Unique,
|
||||
VariantNoDigitalContent,
|
||||
CannotManageProductWithoutVariant,
|
||||
ProductNotAssignedToChannel,
|
||||
UnsupportedMediaProvider,
|
||||
PreorderVariantCannotBeDeactivated,
|
||||
}
|
||||
|
||||
#[derive(cynic::InputObject, Debug)]
|
||||
pub struct MetadataInput<'a> {
|
||||
pub key: &'a str,
|
||||
pub value: &'a str,
|
||||
}
|
8
app-template-ui/src/routes/api/manifest.rs
Normal file
8
app-template-ui/src/routes/api/manifest.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use axum::{extract::State, Json};
|
||||
use saleor_app_sdk::manifest::AppManifest;
|
||||
|
||||
use crate::{app::AppState, error_template::AxumError};
|
||||
|
||||
pub fn manifest(State(state): State<AppState>) -> Result<Json<AppManifest>, AxumError> {
|
||||
Ok(Json(state.manifest))
|
||||
}
|
3
app-template-ui/src/routes/api/mod.rs
Normal file
3
app-template-ui/src/routes/api/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod manifest;
|
||||
pub mod register;
|
||||
pub mod webhooks;
|
41
app-template-ui/src/routes/api/register.rs
Normal file
41
app-template-ui/src/routes/api/register.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::Json,
|
||||
extract::State,
|
||||
http::{HeaderMap, StatusCode},
|
||||
};
|
||||
use saleor_app_sdk::{AuthData, AuthToken};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{app::AppState, error_template::AxumError};
|
||||
|
||||
|
||||
pub async fn register(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
Json(auth_token): Json<AuthToken>,
|
||||
) -> Result<StatusCode, AxumError> {
|
||||
debug!(
|
||||
"/api/register:\nsaleor_api_url:{:?}\nauth_token:{:?}",
|
||||
headers.get("saleor-api-url"),
|
||||
auth_token
|
||||
);
|
||||
|
||||
if auth_token.auth_token.is_empty() {
|
||||
return Err(anyhow::anyhow!("missing auth_token").into());
|
||||
}
|
||||
let app = state.saleor_app.lock().await;
|
||||
let saleor_api_url = headers.get("saleor-api-url").context("missing api field")?;
|
||||
let saleor_api_url = saleor_api_url.to_str()?.to_owned();
|
||||
let auth_data = AuthData {
|
||||
jwks: None,
|
||||
token: auth_token.auth_token,
|
||||
domain: Some(state.config.app_api_base_url),
|
||||
app_id: state.manifest.id,
|
||||
saleor_api_url: saleor_api_url.clone(),
|
||||
};
|
||||
app.apl.set(auth_data).await?;
|
||||
|
||||
info!("registered app for{:?}", &saleor_api_url);
|
||||
Ok(StatusCode::OK)
|
||||
}
|
70
app-template-ui/src/routes/api/webhooks.rs
Normal file
70
app-template-ui/src/routes/api/webhooks.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use axum::{
|
||||
extract::{Json, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
};
|
||||
use cynic::{http::SurfExt, MutationBuilder};
|
||||
use saleor_app_sdk::{
|
||||
headers::{SALEOR_API_URL_HEADER,SALEOR_EVENT_HEADER},
|
||||
webhooks::{
|
||||
utils::{get_webhook_event_type, EitherWebhookType},
|
||||
AsyncWebhookEventType,
|
||||
},
|
||||
};
|
||||
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
app::AppState, error_template::AxumError, queries::{event_products_updated::ProductUpdated, product_metadata_update::{MetadataInput, UpdateProductMetadata, UpdateProductMetadataVariables}} };
|
||||
|
||||
pub async fn webhooks(
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>,
|
||||
//Will try to convert req body to ProductUpdated type, else returns 400
|
||||
Json(product): Json<ProductUpdated>,
|
||||
) -> Result<StatusCode, AxumError> {
|
||||
debug!("/api/webhooks");
|
||||
debug!("req: {:?}", product);
|
||||
debug!("headers: {:?}", headers);
|
||||
|
||||
let url = headers
|
||||
.get(SALEOR_API_URL_HEADER).ok_or(AxumError::MissingHeader(SALEOR_API_URL_HEADER.to_owned()))?;
|
||||
let event_type = get_webhook_event_type(&headers).map_err(|_|AxumError::MissingHeader(SALEOR_EVENT_HEADER.to_owned()))?;
|
||||
if let EitherWebhookType::Async(a) = event_type {
|
||||
match a {
|
||||
AsyncWebhookEventType::ProductUpdated
|
||||
| AsyncWebhookEventType::ProductCreated
|
||||
| AsyncWebhookEventType::ProductDeleted => {
|
||||
update_product(product, url.to_str()?, state).await?
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
info!("got webhooks!");
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
async fn update_product(
|
||||
product: ProductUpdated,
|
||||
saleor_api_url: &str,
|
||||
state: AppState,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
debug!("Product got changed!");
|
||||
if let Some(product) = product.product {
|
||||
let operation = UpdateProductMetadata::build(UpdateProductMetadataVariables {
|
||||
product_id: &product.id,
|
||||
metadata: Some(vec![MetadataInput {
|
||||
key: "helloloo",
|
||||
value: "hiiiihii",
|
||||
}]),
|
||||
});
|
||||
let saleor_app = state.saleor_app.lock().await;
|
||||
let auth_data = saleor_app.apl.get(saleor_api_url).await?;
|
||||
let result = surf::post(saleor_api_url)
|
||||
.header("Authorization", format!("bearer {}", auth_data.token))
|
||||
.run_graphql(operation)
|
||||
.await;
|
||||
debug!("update product result : {:?}", result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
#[cfg(feature = "ssr")]
|
||||
pub mod api;
|
||||
pub mod home;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
use leptos::*;
|
|
@ -1 +0,0 @@
|
|||
pub mod functions;
|
|
@ -5,16 +5,18 @@
|
|||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
@supports (font-variation-settings: normal) {
|
||||
body {
|
||||
@apply bg-brand-white;
|
||||
font-family: "'Inter var',sans-serif";
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-brand-black;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-brand-sunset-400;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply font-serif text-default1 bg-default1;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -23,22 +25,22 @@
|
|||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-sans my-4 text-brand-sea-300 text-2xl;
|
||||
@apply my-4 text-2xl text-default1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-brand-sea-300 text-xl;
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-brand-sea-500 text-base;
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply font-serif my-2 text-base;
|
||||
@apply my-2 text-base;
|
||||
}
|
||||
|
||||
input:disabled,
|
||||
|
@ -66,13 +68,202 @@
|
|||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.header-gradient {
|
||||
background: theme("colors.brand-sea.400");
|
||||
background: linear-gradient(25deg,
|
||||
theme("colors.brand-sunset.500") 0%,
|
||||
theme("colors.brand-sea.300") 45%,
|
||||
theme("colors.brand-sea.300") 55%,
|
||||
theme("colors.brand-sunset.500") 100%);
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Thin.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-ThinItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-ExtraLight.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-ExtraLightItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Light.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-LightItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Regular.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Italic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Medium.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-MediumItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-SemiBold.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-SemiBoldItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Bold.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-ExtraBold.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-ExtraBoldItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-Black.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-BlackItalic.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter var";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-roman.var.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter var";
|
||||
src:
|
||||
local("Inter"),
|
||||
url("/fonts/Inter-italic.var.woff2") format("woff2");
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/** STYLES TAKEN PARTIALLY FROM SALEORS MACAW-UI**/
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: {
|
||||
|
@ -5,8 +6,7 @@ module.exports = {
|
|||
},
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["Space Grotesk", "sans-serif"],
|
||||
serif: ["PT Serif", "serif"],
|
||||
serif: ["Inter", "sans-serif"],
|
||||
},
|
||||
fontSize: {
|
||||
xs: "0.75rem",
|
||||
|
@ -26,30 +26,71 @@ module.exports = {
|
|||
max: "999999px",
|
||||
},
|
||||
extend: {
|
||||
backgroundColor: {
|
||||
accent1: "hsla(215, 100%, 62%, 1)",
|
||||
accent1Hovered: "hsla(215, 100%, 51%, 0.16)",
|
||||
accent1Pressed: "hsla(215, 100%, 51%, 0.32)",
|
||||
buttonCriticalDisabled: "hsla(204, 16%, 94%, 1)",
|
||||
buttonCriticalPrimary: "hsla(11, 100%, 56%, 1)",
|
||||
buttonCriticalPrimaryFocused: "hsla(11, 100%, 42%, 1)",
|
||||
buttonCriticalPrimaryHovered: "hsla(11, 100%, 42%, 1)",
|
||||
buttonCriticalPrimaryPressed: "hsla(11, 100%, 29%, 1)",
|
||||
buttonDefaultDisabled: "hsla(211, 32%, 21%, 1)",
|
||||
buttonDefaultPrimary: "hsla(0, 0%, 100%, 1)",
|
||||
buttonDefaultPrimaryFocused: "hsla(210, 24%, 86%, 1)",
|
||||
buttonDefaultPrimaryHovered: "hsla(211, 24%, 86%, 1)",
|
||||
buttonDefaultPrimaryPressed: "hsla(211, 16%, 68%, 1)",
|
||||
buttonDefaultSecondary: "hsla(232, 17%, 18%, 1)",
|
||||
buttonDefaultSecondaryFocused: "hsla(211, 32%, 19%, 1)",
|
||||
buttonDefaultSecondaryHovered: "hsla(211, 32%, 19%, 1)",
|
||||
buttonDefaultSecondaryPressed: "hsla(211, 24%, 26%, 1)",
|
||||
buttonDefaultTertiary: "hsla(180, 4%, 15%, 0)",
|
||||
buttonDefaultTertiaryFocused: "hsla(0, 0%, 100%, 0.06)",
|
||||
buttonDefaultTertiaryHovered: "hsla(0, 0%, 100%, 0.06)",
|
||||
buttonDefaultTertiaryPressed: "hsla(0, 0%, 100%, 0.12)",
|
||||
critical1: "hsla(11, 100%, 96%, 1)",
|
||||
critical1Focused: "hsla(11, 100%, 46%, 0.2)",
|
||||
critical1Hovered: "hsla(11, 100%, 46%, 0.2)",
|
||||
critical1Pressed: "hsla(11, 100%, 46%, 0.32)",
|
||||
critical2: "hsla(11, 100%, 56%, 1)",
|
||||
default1: "hsla(232, 17%, 18%, 1)",
|
||||
default1Focused: "hsla(0, 0%, 100%, 0.06)",
|
||||
default1Hovered: "hsla(0, 0%, 100%, 0.06)",
|
||||
default1Pressed: "hsla(0, 0%, 100%, 0.12)",
|
||||
default2: "hsla(231, 17%, 16%, 1)",
|
||||
default3: "hsla(211, 42%, 12%, 1)",
|
||||
defaultDisabled: "hsla(211, 32%, 21%, 1)",
|
||||
info1: "hsla(215, 100%, 62%, 1)",
|
||||
success1: "hsla(173, 100%, 32%, 1)",
|
||||
warning1: "hsla(42, 100%, 84%, 1)",
|
||||
},
|
||||
borderColor: {
|
||||
accent1: "hsla(215, 100%, 39%, 1)",
|
||||
critical1: "hsla(11, 100%, 35%, 1)",
|
||||
default1: "hsla(210, 32%, 25%, 1)",
|
||||
default1Focused: "hsla(212, 24%, 32%, 1)",
|
||||
default1Hovered: "hsla(210, 32%, 25%, 1)",
|
||||
defaultDisabled: "hsla(231, 18%, 23%, 1)",
|
||||
default2: "hsla(211, 21%, 39%, 1)",
|
||||
info1: "hsla(210, 32%, 25%, 1)",
|
||||
success1: "hsl(173, 79%, 62%, 1)",
|
||||
warning1: "hsla(36, 44%, 50%, 1)",
|
||||
},
|
||||
colors: {
|
||||
"brand-sea": {
|
||||
100: "#C5ECE0",
|
||||
200: "#17C3B2",
|
||||
300: "#1DA0A8",
|
||||
400: "#227C9D",
|
||||
500: "#094074",
|
||||
},
|
||||
"brand-sunset": {
|
||||
100: "#FEF9EF",
|
||||
200: "#FFEED1",
|
||||
300: "#FFE2B3",
|
||||
400: "#FFD795",
|
||||
500: "#FFCB77",
|
||||
},
|
||||
"brand-red": {
|
||||
100: "#FED6D0",
|
||||
200: "#FEB3B1",
|
||||
300: "#FE9092",
|
||||
400: "#FE7F83",
|
||||
500: "#FE6D73",
|
||||
},
|
||||
"brand-black": "#161a1e",
|
||||
"brand-white": "#fbfbfb",
|
||||
accent1: "hsla(215, 100%, 83%, 1)",
|
||||
buttonCriticalDisabled: "hsla(212, 14%, 67%, 1)",
|
||||
buttonCriticalPrimary: "hsla(0, 0%, 100%, 1)",
|
||||
buttonDefaultPrimary: "hsla(212, 44%, 13%, 1)",
|
||||
buttonDefaultSecondary: "hsla(0, 0%, 100%, 1)",
|
||||
buttonDefaultTertiary: "hsla(0, 0%, 100%, 1)",
|
||||
critical1: "hsla(11, 100%, 82%, 1)",
|
||||
critical2: "hsla(11, 100%, 58%, 1)",
|
||||
default1: "hsla(0, 0%, 100%, 1)",
|
||||
default2: "hsla(230, 10%, 53%, 1)",
|
||||
defaultDisabled: "hsla(212, 19%, 39%, 1)",
|
||||
info1: "hsla(215, 100%, 83%, 1)",
|
||||
success1: "hsla(173, 79%, 62%, 1)",
|
||||
warning1: "hsla(36, 44%, 50%, 1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue