Update to Axum 0.8
This commit is contained in:
parent
975d1ceee2
commit
e355800f98
20 changed files with 1377 additions and 1199 deletions
136
Cargo.lock
generated
136
Cargo.lock
generated
|
|
@ -100,11 +100,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-wincon"
|
name = "anstyle-wincon"
|
||||||
version = "3.0.6"
|
version = "3.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
"once_cell",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -148,14 +149,14 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"axum-core 0.5.0",
|
||||||
"axum-core",
|
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
|
@ -199,26 +200,43 @@ dependencies = [
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-extra"
|
name = "axum-core"
|
||||||
version = "0.9.6"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
|
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
|
||||||
"axum-core",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
|
||||||
"fastrand",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"mime",
|
"mime",
|
||||||
"multer",
|
"pin-project-lite",
|
||||||
|
"rustversion",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core 0.5.0",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.2.0",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"serde",
|
"serde",
|
||||||
"tower",
|
"tower",
|
||||||
|
|
@ -228,9 +246,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-macros"
|
name = "axum-macros"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -335,9 +353,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.7"
|
version = "1.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
|
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
@ -623,15 +641,6 @@ version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "encoding_rs"
|
|
||||||
version = "0.8.35"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -1416,9 +1425,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
|
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
@ -1511,9 +1520,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.22"
|
version = "0.4.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
|
|
@ -1526,9 +1535,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
|
|
@ -1570,9 +1579,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
|
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
]
|
]
|
||||||
|
|
@ -1588,23 +1597,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "multer"
|
|
||||||
version = "3.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"encoding_rs",
|
|
||||||
"futures-util",
|
|
||||||
"http 1.2.0",
|
|
||||||
"httparse",
|
|
||||||
"memchr",
|
|
||||||
"mime",
|
|
||||||
"spin",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
|
@ -1851,9 +1843,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
@ -2028,7 +2020,7 @@ version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc64d77bb950f6498d0fc64b028d168fcb4e56ac31b66a8ae05f64d3b0c218b6"
|
checksum = "cc64d77bb950f6498d0fc64b028d168fcb4e56ac31b66a8ae05f64d3b0c218b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core 0.4.5",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
"rinja",
|
"rinja",
|
||||||
]
|
]
|
||||||
|
|
@ -3059,20 +3051,21 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
|
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -3084,9 +3077,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.49"
|
version = "0.4.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
|
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|
@ -3097,9 +3090,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
|
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
|
@ -3107,9 +3100,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
|
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -3120,15 +3113,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.99"
|
version = "0.2.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.76"
|
version = "0.3.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
|
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|
||||||
axum = { version = "0.7", features = ["macros"] }
|
axum = { version = "0.8", features = ["macros"] }
|
||||||
axum-extra = { version = "0.9", features = ["cookie"] }
|
axum-extra = { version = "0.10", features = ["cookie"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower = { version = "0.5", features = ["util"] }
|
tower = { version = "0.5", features = ["util"] }
|
||||||
tower-http = { version = "0.6", features = ["fs", "trace"] }
|
tower-http = { version = "0.6", features = ["fs", "trace"] }
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@ body {
|
||||||
|
|
||||||
.recipe-item {
|
.recipe-item {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
// Transparent border: to keep same size than '.recipe-item-current'.
|
||||||
|
border: 0.1em solid rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-item-current {
|
.recipe-item-current {
|
||||||
|
|
@ -111,6 +113,8 @@ body {
|
||||||
.content {
|
.content {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
margin-left: 0px;
|
||||||
|
|
||||||
background-color: $color-2;
|
background-color: $color-2;
|
||||||
border: 0.1em solid $color-3;
|
border: 0.1em solid $color-3;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
|
|
@ -122,13 +126,14 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#recipe-edit {
|
#recipe-edit {
|
||||||
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
border: 0.1em solid lighten($color-3, 30%);
|
border: 0.1em solid lighten($color-3, 30%);
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step {
|
.step {
|
||||||
|
|
@ -139,9 +144,11 @@ body {
|
||||||
border: 0.1em solid lighten($color-3, 30%);
|
border: 0.1em solid lighten($color-3, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone-group,
|
.dropzone {
|
||||||
.dropzone-step {
|
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use sqlx::{
|
||||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
||||||
Pool, Sqlite, Transaction,
|
Pool, Sqlite, Transaction,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
|
|
@ -21,7 +20,7 @@ pub mod user;
|
||||||
|
|
||||||
const CURRENT_DB_VERSION: u32 = 1;
|
const CURRENT_DB_VERSION: u32 = 1;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum DBError {
|
pub enum DBError {
|
||||||
#[error("Sqlx error: {0}")]
|
#[error("Sqlx error: {0}")]
|
||||||
Sqlx(#[from] sqlx::Error),
|
Sqlx(#[from] sqlx::Error),
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,28 @@ WHERE [Step].[id] = $1 AND [user_id] = $2
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn can_edit_recipe_all_steps(&self, user_id: i64, steps_ids: &[i64]) -> Result<bool> {
|
||||||
|
let params = (0..steps_ids.len())
|
||||||
|
.map(|n| format!("${}", n + 2))
|
||||||
|
.join(", ");
|
||||||
|
let query_str = format!(
|
||||||
|
r#"
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM [Recipe]
|
||||||
|
INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
|
||||||
|
INNER JOIN [Step] ON [Step].[group_id] = [Group].[id]
|
||||||
|
WHERE [Step].[id] IN ({}) AND [user_id] = $1
|
||||||
|
"#,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||||
|
for id in steps_ids {
|
||||||
|
query = query.bind(id);
|
||||||
|
}
|
||||||
|
Ok(query.fetch_one(&self.pool).await? == steps_ids.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn can_edit_recipe_ingredient(
|
pub async fn can_edit_recipe_ingredient(
|
||||||
&self,
|
&self,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
|
|
@ -475,10 +497,22 @@ ORDER BY [name]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_recipe_group(&self, recipe_id: i64) -> Result<i64> {
|
pub async fn add_recipe_group(&self, recipe_id: i64) -> Result<i64> {
|
||||||
let db_result = sqlx::query("INSERT INTO [Group] ([recipe_id]) VALUES ($1)")
|
let mut tx = self.tx().await?;
|
||||||
|
|
||||||
|
let last_order = sqlx::query_scalar(
|
||||||
|
"SELECT [order] FROM [Group] WHERE [recipe_id] = $1 ORDER BY [order] DESC LIMIT 1",
|
||||||
|
)
|
||||||
.bind(recipe_id)
|
.bind(recipe_id)
|
||||||
.execute(&self.pool)
|
.fetch_optional(&mut *tx)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(-1);
|
||||||
|
|
||||||
|
let db_result = sqlx::query("INSERT INTO [Group] ([recipe_id, [order]) VALUES ($1, $2)")
|
||||||
|
.bind(recipe_id)
|
||||||
|
.bind(last_order + 1)
|
||||||
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(db_result.last_insert_rowid())
|
Ok(db_result.last_insert_rowid())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,6 +588,22 @@ ORDER BY [name]
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_steps_order(&self, step_ids: &[i64]) -> Result<()> {
|
||||||
|
let mut tx = self.tx().await?;
|
||||||
|
|
||||||
|
for (order, id) in step_ids.iter().enumerate() {
|
||||||
|
sqlx::query("UPDATE [Step] SET [order] = $2 WHERE [id] = $1")
|
||||||
|
.bind(id)
|
||||||
|
.bind(order as i64)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn add_recipe_ingredient(&self, step_id: i64) -> Result<i64> {
|
pub async fn add_recipe_ingredient(&self, step_id: i64) -> Result<i64> {
|
||||||
let db_result = sqlx::query("INSERT INTO [Ingredient] ([step_id]) VALUES ($1)")
|
let db_result = sqlx::query("INSERT INTO [Ingredient] ([step_id]) VALUES ($1)")
|
||||||
.bind(step_id)
|
.bind(step_id)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use axum::{
|
||||||
extract::{ConnectInfo, Extension, FromRef, Request, State},
|
extract::{ConnectInfo, Extension, FromRef, Request, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{Response, Result},
|
response::Response,
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
|
@ -55,6 +55,23 @@ impl axum::response::IntoResponse for db::DBError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum AppError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
Database(#[from] db::DBError),
|
||||||
|
|
||||||
|
#[error("Template error: {0}")]
|
||||||
|
Render(#[from] rinja::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, AppError>;
|
||||||
|
|
||||||
|
impl axum::response::IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
|
const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
|
||||||
|
|
||||||
|
|
@ -183,8 +200,8 @@ async fn main() {
|
||||||
)
|
)
|
||||||
// Recipes.
|
// Recipes.
|
||||||
.route("/recipe/new", get(services::recipe::create))
|
.route("/recipe/new", get(services::recipe::create))
|
||||||
.route("/recipe/edit/:id", get(services::recipe::edit_recipe))
|
.route("/recipe/edit/{id}", get(services::recipe::edit_recipe))
|
||||||
.route("/recipe/view/:id", get(services::recipe::view))
|
.route("/recipe/view/{id}", get(services::recipe::view))
|
||||||
// User.
|
// User.
|
||||||
.route(
|
.route(
|
||||||
"/user/edit",
|
"/user/edit",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
async_trait,
|
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::{FromRequest, Request},
|
extract::{FromRequest, Request},
|
||||||
http::{header, StatusCode},
|
http::{header, StatusCode},
|
||||||
|
|
@ -11,7 +10,6 @@ use crate::ron_utils;
|
||||||
|
|
||||||
pub struct ExtractRon<T: DeserializeOwned>(pub T);
|
pub struct ExtractRon<T: DeserializeOwned>(pub T);
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<S, T> FromRequest<S> for ExtractRon<T>
|
impl<S, T> FromRequest<S> for ExtractRon<T>
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
debug_handler,
|
||||||
extract::{Extension, Query, State},
|
extract::{Extension, Query, State},
|
||||||
response::{IntoResponse, Result},
|
response::{Html, IntoResponse},
|
||||||
};
|
};
|
||||||
|
use rinja::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
// use tracing::{event, Level};
|
// use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
translation,
|
translation, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -37,5 +38,5 @@ pub async fn recipes_list_fragments(
|
||||||
},
|
},
|
||||||
current_id: current_recipe.current_recipe_id,
|
current_id: current_recipe.current_recipe_id,
|
||||||
};
|
};
|
||||||
Ok(RecipesListFragmentTemplate { tr, recipes })
|
Ok(Html(RecipesListFragmentTemplate { tr, recipes }.render()?))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ use axum::{
|
||||||
extract::{Extension, Request, State},
|
extract::{Extension, Request, State},
|
||||||
http::{header, StatusCode},
|
http::{header, StatusCode},
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response, Result},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use rinja::Template;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
ron_utils, translation,
|
ron_utils, translation, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod fragments;
|
pub mod fragments;
|
||||||
|
|
@ -31,12 +32,15 @@ pub async fn ron_error_to_html(
|
||||||
Ok(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(),
|
Ok(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(),
|
||||||
Err(error) => error.to_string(),
|
Err(error) => error.to_string(),
|
||||||
};
|
};
|
||||||
return Ok(MessageTemplate {
|
return Ok(Html(
|
||||||
|
MessageTemplate {
|
||||||
user: None,
|
user: None,
|
||||||
message: &message,
|
message: &message,
|
||||||
as_code: true,
|
as_code: true,
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +70,7 @@ pub async fn home_page(
|
||||||
current_id: None,
|
current_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HomeTemplate { user, recipes, tr })
|
Ok(Html(HomeTemplate { user, recipes, tr }.render()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
///// 404 /////
|
///// 404 /////
|
||||||
|
|
@ -75,9 +79,9 @@ pub async fn home_page(
|
||||||
pub async fn not_found(
|
pub async fn not_found(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<impl IntoResponse> {
|
||||||
(
|
Ok((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
MessageTemplate::new_with_user("404: Not found", tr, user),
|
Html(MessageTemplate::new_with_user("404: Not found", tr, user).render()?),
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
debug_handler,
|
||||||
extract::{Extension, Path, State},
|
extract::{Extension, Path, State},
|
||||||
response::{IntoResponse, Redirect, Response, Result},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
|
use rinja::Template;
|
||||||
// use tracing::{event, Level};
|
// use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
translation::{self, Sentence},
|
translation::{self, Sentence},
|
||||||
|
Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
|
|
@ -21,7 +23,7 @@ pub async fn create(
|
||||||
let recipe_id = connection.create_recipe(user.id).await?;
|
let recipe_id = connection.create_recipe(user.id).await?;
|
||||||
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
|
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
|
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,24 +47,33 @@ pub async fn edit_recipe(
|
||||||
current_id: Some(recipe_id),
|
current_id: Some(recipe_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RecipeEditTemplate {
|
Ok(Html(
|
||||||
|
RecipeEditTemplate {
|
||||||
user: Some(user),
|
user: Some(user),
|
||||||
tr,
|
tr,
|
||||||
recipes,
|
recipes,
|
||||||
recipe,
|
recipe,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(
|
Ok(
|
||||||
|
Html(
|
||||||
MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr)
|
MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr)
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).into_response())
|
Ok(
|
||||||
|
Html(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).render()?)
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
|
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,11 +89,14 @@ pub async fn view(
|
||||||
if !recipe.is_published
|
if !recipe.is_published
|
||||||
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
|
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
|
||||||
{
|
{
|
||||||
return Ok(MessageTemplate::new_with_user(
|
return Ok(Html(
|
||||||
|
MessageTemplate::new_with_user(
|
||||||
&tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
|
&tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,17 +117,20 @@ pub async fn view(
|
||||||
current_id: Some(recipe_id),
|
current_id: Some(recipe_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(RecipeViewTemplate {
|
Ok(Html(
|
||||||
|
RecipeViewTemplate {
|
||||||
user,
|
user,
|
||||||
tr,
|
tr,
|
||||||
recipes,
|
recipes,
|
||||||
recipe,
|
recipe,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
None => Ok(
|
None => Ok(Html(
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::RecipeNotFound), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::RecipeNotFound), tr, user).render()?,
|
||||||
.into_response(),
|
)
|
||||||
),
|
.into_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,25 @@ async fn check_user_rights_recipe_step(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_user_rights_recipe_steps(
|
||||||
|
connection: &db::Connection,
|
||||||
|
user: &Option<model::User>,
|
||||||
|
step_ids: &[i64],
|
||||||
|
) -> Result<()> {
|
||||||
|
if user.is_none()
|
||||||
|
|| !connection
|
||||||
|
.can_edit_recipe_all_steps(user.as_ref().unwrap().id, step_ids)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Err(ErrorResponse::from(ron_error(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
NOT_AUTHORIZED_MESSAGE,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn check_user_rights_recipe_ingredient(
|
async fn check_user_rights_recipe_ingredient(
|
||||||
connection: &db::Connection,
|
connection: &db::Connection,
|
||||||
user: &Option<model::User>,
|
user: &Option<model::User>,
|
||||||
|
|
@ -463,6 +482,17 @@ pub async fn set_step_action(
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn set_step_orders(
|
||||||
|
State(connection): State<db::Connection>,
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
ExtractRon(ron): ExtractRon<common::ron_api::SetStepOrders>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
check_user_rights_recipe_steps(&connection, &user, &ron.step_ids).await?;
|
||||||
|
connection.set_steps_order(&ron.step_ids).await?;
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn add_ingredient(
|
pub async fn add_ingredient(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ use std::{collections::HashMap, net::SocketAddr};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
debug_handler,
|
debug_handler,
|
||||||
extract::{ConnectInfo, Extension, Host, Query, Request, State},
|
extract::{ConnectInfo, Extension, Query, Request, State},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Redirect, Response, Result},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
Form,
|
Form,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, CookieJar},
|
||||||
|
Host,
|
||||||
|
};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use rinja::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
|
@ -20,7 +24,7 @@ use crate::{
|
||||||
email,
|
email,
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
translation::{self, Sentence},
|
translation::{self, Sentence},
|
||||||
utils, AppState,
|
utils, AppState, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// SIGN UP ///
|
/// SIGN UP ///
|
||||||
|
|
@ -30,14 +34,17 @@ pub async fn sign_up_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
Ok(SignUpFormTemplate {
|
Ok(Html(
|
||||||
|
SignUpFormTemplate {
|
||||||
user,
|
user,
|
||||||
tr,
|
tr,
|
||||||
email: String::new(),
|
email: String::new(),
|
||||||
message: "",
|
message: "",
|
||||||
message_email: "",
|
message_email: "",
|
||||||
message_password: "",
|
message_password: "",
|
||||||
})
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
@ -75,7 +82,8 @@ pub async fn sign_up_post(
|
||||||
Sentence::InvalidPassword,
|
Sentence::InvalidPassword,
|
||||||
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
||||||
);
|
);
|
||||||
Ok(SignUpFormTemplate {
|
Ok(Html(
|
||||||
|
SignUpFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: form_data.email.clone(),
|
email: form_data.email.clone(),
|
||||||
message_email: match error {
|
message_email: match error {
|
||||||
|
|
@ -95,6 +103,8 @@ pub async fn sign_up_post(
|
||||||
},
|
},
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,12 +150,11 @@ pub async fn sign_up_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => {
|
Ok(()) => Ok(Html(
|
||||||
Ok(
|
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user)
|
||||||
.into_response(),
|
.render()?,
|
||||||
)
|
)
|
||||||
}
|
.into_response()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
|
error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
|
||||||
|
|
@ -172,7 +181,14 @@ pub async fn sign_up_validation(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
return Ok((
|
return Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::ValidationUserAlreadyExists), tr, user),
|
Html(
|
||||||
|
MessageTemplate::new_with_user(
|
||||||
|
tr.t(Sentence::ValidationUserAlreadyExists),
|
||||||
|
tr,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
||||||
|
|
@ -194,34 +210,46 @@ pub async fn sign_up_validation(
|
||||||
let user = connection.load_user(user_id).await?;
|
let user = connection.load_user(user_id).await?;
|
||||||
Ok((
|
Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
tr.t(Sentence::SignUpEmailValidationSuccess),
|
tr.t(Sentence::SignUpEmailValidationSuccess),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
db::user::ValidationResult::ValidationExpired => Ok((
|
db::user::ValidationResult::ValidationExpired => Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
tr.t(Sentence::SignUpValidationExpired),
|
tr.t(Sentence::SignUpValidationExpired),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
db::user::ValidationResult::UnknownUser => Ok((
|
db::user::ValidationResult::UnknownUser => Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
tr.t(Sentence::SignUpValidationErrorTryAgain),
|
tr.t(Sentence::SignUpValidationErrorTryAgain),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok((
|
None => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user),
|
Html(
|
||||||
|
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
|
||||||
|
.render()?,
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,12 +261,15 @@ pub async fn sign_in_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
Ok(SignInFormTemplate {
|
Ok(Html(
|
||||||
|
SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
tr,
|
tr,
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
})
|
}
|
||||||
|
.render()?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
@ -270,22 +301,28 @@ pub async fn sign_in_post(
|
||||||
{
|
{
|
||||||
db::user::SignInResult::AccountNotValidated => Ok((
|
db::user::SignInResult::AccountNotValidated => Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
SignInFormTemplate {
|
SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: &form_data.email,
|
email: &form_data.email,
|
||||||
message: tr.t(Sentence::AccountMustBeValidatedFirst),
|
message: tr.t(Sentence::AccountMustBeValidatedFirst),
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)),
|
)),
|
||||||
db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok((
|
db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
SignInFormTemplate {
|
SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: &form_data.email,
|
email: &form_data.email,
|
||||||
message: tr.t(Sentence::WrongEmailOrPassword),
|
message: tr.t(Sentence::WrongEmailOrPassword),
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)),
|
)),
|
||||||
db::user::SignInResult::Ok(token, _user_id) => {
|
db::user::SignInResult::Ok(token, _user_id) => {
|
||||||
|
|
@ -319,18 +356,22 @@ pub async fn ask_reset_password_get(
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
Ok(
|
Ok(Html(
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user)
|
||||||
.into_response(),
|
.render()?,
|
||||||
)
|
)
|
||||||
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(AskResetPasswordTemplate {
|
Ok(Html(
|
||||||
|
AskResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
tr,
|
tr,
|
||||||
email: "",
|
email: "",
|
||||||
message: "",
|
message: "",
|
||||||
message_email: "",
|
message_email: "",
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -363,7 +404,8 @@ pub async fn ask_reset_password_post(
|
||||||
user: Option<model::User>,
|
user: Option<model::User>,
|
||||||
tr: translation::Tr,
|
tr: translation::Tr,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
Ok(AskResetPasswordTemplate {
|
Ok(Html(
|
||||||
|
AskResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
email,
|
email,
|
||||||
message_email: match error {
|
message_email: match error {
|
||||||
|
|
@ -375,12 +417,16 @@ pub async fn ask_reset_password_post(
|
||||||
tr.t(Sentence::AskResetEmailAlreadyResetError)
|
tr.t(Sentence::AskResetEmailAlreadyResetError)
|
||||||
}
|
}
|
||||||
AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
|
AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
|
||||||
AskResetPasswordError::UnableSendEmail => tr.t(Sentence::UnableToSendResetEmail),
|
AskResetPasswordError::UnableSendEmail => {
|
||||||
|
tr.t(Sentence::UnableToSendResetEmail)
|
||||||
|
}
|
||||||
AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
|
AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
|
||||||
_ => "",
|
_ => "",
|
||||||
},
|
},
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,12 +478,11 @@ pub async fn ask_reset_password_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => {
|
Ok(()) => Ok(Html(
|
||||||
Ok(
|
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user)
|
||||||
.into_response(),
|
.render()?,
|
||||||
)
|
)
|
||||||
}
|
.into_response()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
error_response(
|
error_response(
|
||||||
|
|
@ -477,25 +522,30 @@ pub async fn reset_password_get(
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Ok(ResetPasswordTemplate {
|
Ok(Html(
|
||||||
|
ResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
tr,
|
tr,
|
||||||
reset_token,
|
reset_token,
|
||||||
message: "",
|
message: "",
|
||||||
message_password: "",
|
message_password: "",
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(
|
Ok(Html(
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
||||||
.into_response(),
|
.render()?,
|
||||||
)
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(
|
Ok(Html(
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
||||||
.into_response(),
|
.render()?,
|
||||||
)
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,7 +580,8 @@ pub async fn reset_password_post(
|
||||||
Sentence::InvalidPassword,
|
Sentence::InvalidPassword,
|
||||||
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
||||||
);
|
);
|
||||||
Ok(ResetPasswordTemplate {
|
Ok(Html(
|
||||||
|
ResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
reset_token: &form_data.reset_token,
|
reset_token: &form_data.reset_token,
|
||||||
message_password: match error {
|
message_password: match error {
|
||||||
|
|
@ -545,6 +596,8 @@ pub async fn reset_password_post(
|
||||||
},
|
},
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -566,12 +619,10 @@ pub async fn reset_password_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(db::user::ResetPasswordResult::Ok) => {
|
Ok(db::user::ResetPasswordResult::Ok) => Ok(Html(
|
||||||
Ok(
|
MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user).render()?,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user)
|
|
||||||
.into_response(),
|
|
||||||
)
|
)
|
||||||
}
|
.into_response()),
|
||||||
Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
|
Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
|
||||||
error_response(ResetPasswordError::TokenExpired, &form_data, user, tr)
|
error_response(ResetPasswordError::TokenExpired, &form_data, user, tr)
|
||||||
}
|
}
|
||||||
|
|
@ -585,8 +636,9 @@ pub async fn reset_password_post(
|
||||||
pub async fn edit_user_get(
|
pub async fn edit_user_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Response {
|
) -> Result<Response> {
|
||||||
if let Some(user) = user {
|
Ok(if let Some(user) = user {
|
||||||
|
Html(
|
||||||
ProfileTemplate {
|
ProfileTemplate {
|
||||||
username: &user.name,
|
username: &user.name,
|
||||||
email: &user.email,
|
email: &user.email,
|
||||||
|
|
@ -596,10 +648,12 @@ pub async fn edit_user_get(
|
||||||
user: Some(user.clone()),
|
user: Some(user.clone()),
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response()
|
Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
@ -640,7 +694,8 @@ pub async fn edit_user_post(
|
||||||
Sentence::InvalidPassword,
|
Sentence::InvalidPassword,
|
||||||
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
||||||
);
|
);
|
||||||
Ok(ProfileTemplate {
|
Ok(Html(
|
||||||
|
ProfileTemplate {
|
||||||
user: Some(user),
|
user: Some(user),
|
||||||
username: &form_data.name,
|
username: &form_data.name,
|
||||||
email: &form_data.email,
|
email: &form_data.email,
|
||||||
|
|
@ -661,6 +716,8 @@ pub async fn edit_user_post(
|
||||||
},
|
},
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -742,7 +799,8 @@ pub async fn edit_user_post(
|
||||||
// Reload after update.
|
// Reload after update.
|
||||||
let user = connection.load_user(user.id).await?;
|
let user = connection.load_user(user.id).await?;
|
||||||
|
|
||||||
Ok(ProfileTemplate {
|
Ok(Html(
|
||||||
|
ProfileTemplate {
|
||||||
user,
|
user,
|
||||||
username: &form_data.name,
|
username: &form_data.name,
|
||||||
email: &form_data.email,
|
email: &form_data.email,
|
||||||
|
|
@ -751,9 +809,11 @@ pub async fn edit_user_post(
|
||||||
message_password: "",
|
message_password: "",
|
||||||
tr,
|
tr,
|
||||||
}
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).into_response())
|
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -770,7 +830,14 @@ pub async fn email_revalidation(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
return Ok((
|
return Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::ValidationUserAlreadyExists), tr, user),
|
Html(
|
||||||
|
MessageTemplate::new_with_user(
|
||||||
|
tr.t(Sentence::ValidationUserAlreadyExists),
|
||||||
|
tr,
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
||||||
|
|
@ -792,30 +859,42 @@ pub async fn email_revalidation(
|
||||||
let user = connection.load_user(user_id).await?;
|
let user = connection.load_user(user_id).await?;
|
||||||
Ok((
|
Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
tr.t(Sentence::ValidationSuccessful),
|
tr.t(Sentence::ValidationSuccessful),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
db::user::ValidationResult::ValidationExpired => Ok((
|
db::user::ValidationResult::ValidationExpired => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user),
|
Html(
|
||||||
|
MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user)
|
||||||
|
.render()?,
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
db::user::ValidationResult::UnknownUser => Ok((
|
db::user::ValidationResult::UnknownUser => Ok((
|
||||||
jar,
|
jar,
|
||||||
|
Html(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
tr.t(Sentence::ValidationErrorTryToSignUpAgain),
|
tr.t(Sentence::ValidationErrorTryToSignUpAgain),
|
||||||
tr,
|
tr,
|
||||||
user,
|
user,
|
||||||
|
)
|
||||||
|
.render()?,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok((
|
None => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user),
|
Html(
|
||||||
|
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
|
||||||
|
.render()?,
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ pub enum Sentence {
|
||||||
NotLoggedIn,
|
NotLoggedIn,
|
||||||
|
|
||||||
DatabaseError,
|
DatabaseError,
|
||||||
|
TemplateError,
|
||||||
|
|
||||||
// Sign in page.
|
// Sign in page.
|
||||||
SignInMenu,
|
SignInMenu,
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@
|
||||||
<input id="input-delete" type="button" value="{{ tr.t(Sentence::RecipeDelete) }}" />
|
<input id="input-delete" type="button" value="{{ tr.t(Sentence::RecipeDelete) }}" />
|
||||||
|
|
||||||
<div id="groups-container">
|
<div id="groups-container">
|
||||||
<div class="dropzone-group"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input id="input-add-group" type="button" value="{{ tr.t(Sentence::RecipeAddAGroup) }}" />
|
<input id="input-add-group" type="button" value="{{ tr.t(Sentence::RecipeAddAGroup) }}" />
|
||||||
|
|
||||||
<div id="hidden-templates">
|
<div id="hidden-templates">
|
||||||
|
|
@ -97,7 +97,6 @@
|
||||||
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}" />
|
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}" />
|
||||||
|
|
||||||
<div class="steps">
|
<div class="steps">
|
||||||
<div class="dropzone-step"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}" />
|
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}" />
|
||||||
|
|
@ -131,6 +130,8 @@
|
||||||
|
|
||||||
<input class="input-ingredient-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}" />
|
<input class="input-ingredient-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dropzone"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
(NotLoggedIn, "No logged in"),
|
(NotLoggedIn, "No logged in"),
|
||||||
|
|
||||||
(DatabaseError, "Database error"),
|
(DatabaseError, "Database error"),
|
||||||
|
(TemplateError, "Template error"),
|
||||||
|
|
||||||
(SignInMenu, "Sign in"),
|
(SignInMenu, "Sign in"),
|
||||||
(SignInTitle, "Sign in"),
|
(SignInTitle, "Sign in"),
|
||||||
|
|
@ -112,7 +113,8 @@
|
||||||
(Save, "Sauvegarder"),
|
(Save, "Sauvegarder"),
|
||||||
(NotLoggedIn, "Pas connecté"),
|
(NotLoggedIn, "Pas connecté"),
|
||||||
|
|
||||||
(DatabaseError, "Erreur de la base de données"),
|
(DatabaseError, "Erreur de la base de données (Database error)"),
|
||||||
|
(TemplateError, "Erreur du moteur de modèles (Template error)"),
|
||||||
|
|
||||||
(SignInMenu, "Se connecter"),
|
(SignInMenu, "Se connecter"),
|
||||||
(SignInTitle, "Se connecter"),
|
(SignInTitle, "Se connecter"),
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,11 @@ pub struct SetStepAction {
|
||||||
pub action: String,
|
pub action: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SetStepOrders {
|
||||||
|
pub step_ids: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct AddRecipeIngredient {
|
pub struct AddRecipeIngredient {
|
||||||
pub step_id: i64,
|
pub step_id: i64,
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ gloo = "0.11"
|
||||||
# code size when deploying.
|
# code size when deploying.
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
console_error_panic_hook = { version = "0.1", optional = true }
|
||||||
|
|
||||||
# [dev-dependencies]
|
|
||||||
# wasm-bindgen-test = "0.3"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Tell `rustc` to optimize for small code size.
|
# Tell `rustc` to optimize for small code size.
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|
|
||||||
|
|
@ -1,875 +0,0 @@
|
||||||
use gloo::{
|
|
||||||
console::log,
|
|
||||||
events::{EventListener, EventListenerOptions},
|
|
||||||
net::http::Request,
|
|
||||||
utils::{document, window},
|
|
||||||
};
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use web_sys::{
|
|
||||||
DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
|
||||||
KeyboardEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
use common::ron_api;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
modal_dialog, request,
|
|
||||||
toast::{self, Level},
|
|
||||||
utils::{by_id, selector, selector_all, selector_and_clone, SelectorExt},
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn reload_recipes_list(current_recipe_id: i64) {
|
|
||||||
match Request::get("/fragments/recipes_list")
|
|
||||||
.query([("current_recipe_id", current_recipe_id.to_string())])
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Err(error) => {
|
|
||||||
toast::show(Level::Info, &format!("Internal server error: {}", error));
|
|
||||||
}
|
|
||||||
Ok(response) => {
|
|
||||||
let list = document().get_element_by_id("recipes-list").unwrap();
|
|
||||||
list.set_outer_html(&response.text().await.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
|
||||||
// Title.
|
|
||||||
{
|
|
||||||
let Some(title) = document().get_element_by_id("input-title") else {
|
|
||||||
return Err(JsValue::from_str("Unable to find 'input-title' element"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let title: HtmlInputElement = title.dyn_into().unwrap();
|
|
||||||
|
|
||||||
// Check if the recipe has been loaded.
|
|
||||||
|
|
||||||
let mut current_title = title.value();
|
|
||||||
EventListener::new(&title.clone(), "blur", move |_event| {
|
|
||||||
if title.value() != current_title {
|
|
||||||
current_title = title.value();
|
|
||||||
let body = ron_api::SetRecipeTitle {
|
|
||||||
recipe_id,
|
|
||||||
title: title.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_title", body).await;
|
|
||||||
reload_recipes_list(recipe_id).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description.
|
|
||||||
{
|
|
||||||
let description: HtmlTextAreaElement = by_id("text-area-description");
|
|
||||||
let mut current_description = description.value();
|
|
||||||
|
|
||||||
EventListener::new(&description.clone(), "blur", move |_event| {
|
|
||||||
if description.value() != current_description {
|
|
||||||
current_description = description.value();
|
|
||||||
let body = ron_api::SetRecipeDescription {
|
|
||||||
recipe_id,
|
|
||||||
description: description.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_description", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Servings.
|
|
||||||
{
|
|
||||||
let servings: HtmlInputElement = by_id("input-servings");
|
|
||||||
let mut current_servings = servings.value_as_number();
|
|
||||||
EventListener::new(&servings.clone(), "input", move |_event| {
|
|
||||||
let n = servings.value_as_number();
|
|
||||||
if n.is_nan() {
|
|
||||||
servings.set_value("");
|
|
||||||
}
|
|
||||||
if n != current_servings {
|
|
||||||
let servings = if n.is_nan() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// TODO: Find a better way to validate integer numbers.
|
|
||||||
let n = n as u32;
|
|
||||||
servings.set_value_as_number(n as f64);
|
|
||||||
Some(n)
|
|
||||||
};
|
|
||||||
current_servings = n;
|
|
||||||
let body = ron_api::SetRecipeServings {
|
|
||||||
recipe_id,
|
|
||||||
servings,
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_servings", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estimated time.
|
|
||||||
{
|
|
||||||
let estimated_time: HtmlInputElement = by_id("input-estimated-time");
|
|
||||||
let mut current_time = estimated_time.value_as_number();
|
|
||||||
|
|
||||||
EventListener::new(&estimated_time.clone(), "input", move |_event| {
|
|
||||||
let n = estimated_time.value_as_number();
|
|
||||||
if n.is_nan() {
|
|
||||||
estimated_time.set_value("");
|
|
||||||
}
|
|
||||||
if n != current_time {
|
|
||||||
let time = if n.is_nan() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// TODO: Find a better way to validate integer numbers.
|
|
||||||
let n = n as u32;
|
|
||||||
estimated_time.set_value_as_number(n as f64);
|
|
||||||
Some(n)
|
|
||||||
};
|
|
||||||
current_time = n;
|
|
||||||
let body = ron_api::SetRecipeEstimatedTime {
|
|
||||||
recipe_id,
|
|
||||||
estimated_time: time,
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_estimated_time", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difficulty.
|
|
||||||
{
|
|
||||||
let difficulty: HtmlSelectElement = by_id("select-difficulty");
|
|
||||||
let mut current_difficulty = difficulty.value();
|
|
||||||
|
|
||||||
EventListener::new(&difficulty.clone(), "blur", move |_event| {
|
|
||||||
if difficulty.value() != current_difficulty {
|
|
||||||
current_difficulty = difficulty.value();
|
|
||||||
|
|
||||||
let body = ron_api::SetRecipeDifficulty {
|
|
||||||
recipe_id,
|
|
||||||
difficulty: ron_api::Difficulty::try_from(
|
|
||||||
current_difficulty.parse::<u32>().unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_difficulty", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags.
|
|
||||||
{
|
|
||||||
spawn_local(async move {
|
|
||||||
let tags: ron_api::Tags =
|
|
||||||
request::get("recipe/get_tags", [("recipe_id", &recipe_id.to_string())])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
create_tag_elements(recipe_id, &tags.tags);
|
|
||||||
});
|
|
||||||
|
|
||||||
fn add_tags(recipe_id: i64, tags: String) {
|
|
||||||
spawn_local(async move {
|
|
||||||
let tag_list: Vec<String> = tags.split_whitespace().map(String::from).collect();
|
|
||||||
if !tag_list.is_empty() {
|
|
||||||
let body = ron_api::Tags {
|
|
||||||
recipe_id,
|
|
||||||
tags: tag_list.clone(),
|
|
||||||
};
|
|
||||||
let _ = request::post::<(), _>("recipe/add_tags", body).await;
|
|
||||||
create_tag_elements(recipe_id, &tag_list);
|
|
||||||
}
|
|
||||||
by_id::<HtmlInputElement>("input-tags").set_value("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_tags: HtmlInputElement = by_id("input-tags");
|
|
||||||
EventListener::new(&input_tags.clone(), "input", move |_event| {
|
|
||||||
let tags = input_tags.value();
|
|
||||||
if tags.ends_with(' ') {
|
|
||||||
add_tags(recipe_id, tags);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
let input_tags: HtmlInputElement = by_id("input-tags");
|
|
||||||
EventListener::new(&input_tags.clone(), "keypress", move |event| {
|
|
||||||
if let Some(keyboard_event) = event.dyn_ref::<KeyboardEvent>() {
|
|
||||||
if keyboard_event.key_code() == 13 {
|
|
||||||
let tags = input_tags.value();
|
|
||||||
add_tags(recipe_id, tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
let input_tags: HtmlInputElement = by_id("input-tags");
|
|
||||||
EventListener::new(&input_tags.clone(), "blur", move |_event| {
|
|
||||||
let tags = input_tags.value();
|
|
||||||
add_tags(recipe_id, tags);
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Language.
|
|
||||||
{
|
|
||||||
let language: HtmlSelectElement = by_id("select-language");
|
|
||||||
let mut current_language = language.value();
|
|
||||||
EventListener::new(&language.clone(), "blur", move |_event| {
|
|
||||||
if language.value() != current_language {
|
|
||||||
current_language = language.value();
|
|
||||||
|
|
||||||
let body = ron_api::SetRecipeLanguage {
|
|
||||||
recipe_id,
|
|
||||||
lang: language.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_language", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is published.
|
|
||||||
{
|
|
||||||
let is_published: HtmlInputElement = by_id("input-is-published");
|
|
||||||
EventListener::new(&is_published.clone(), "input", move |_event| {
|
|
||||||
let body = ron_api::SetIsPublished {
|
|
||||||
recipe_id,
|
|
||||||
is_published: is_published.checked(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_is_published", body).await;
|
|
||||||
reload_recipes_list(recipe_id).await;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete recipe button.
|
|
||||||
let delete_button: HtmlInputElement = by_id("input-delete");
|
|
||||||
EventListener::new(&delete_button, "click", move |_event| {
|
|
||||||
let title: HtmlInputElement = by_id("input-title");
|
|
||||||
spawn_local(async move {
|
|
||||||
if modal_dialog::show(&format!(
|
|
||||||
"Are you sure to delete the recipe '{}'",
|
|
||||||
title.value()
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let body = ron_api::Remove { recipe_id };
|
|
||||||
let _ = request::delete::<(), _>("recipe/remove", body).await;
|
|
||||||
window().location().set_href("/").unwrap();
|
|
||||||
|
|
||||||
// by_id::<Element>(&format!("group-{}", group_id)).remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
let group_dropzone: Element = selector(".dropzone-group");
|
|
||||||
setup_dragzone_events(&group_dropzone);
|
|
||||||
|
|
||||||
fn setup_dragzone_events(dropzone: &Element) {
|
|
||||||
EventListener::new_with_options(
|
|
||||||
dropzone,
|
|
||||||
"dragover",
|
|
||||||
EventListenerOptions::enable_prevent_default(),
|
|
||||||
|event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
let drag_data = event
|
|
||||||
.data_transfer()
|
|
||||||
.unwrap()
|
|
||||||
.get_data("text/plain")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if drag_data.starts_with("group") {
|
|
||||||
event.prevent_default();
|
|
||||||
// event.data_transfer().unwrap().set_effect_allowed("move");
|
|
||||||
log!("drag over");
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.set_class_name("dropzone-group hover");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(dropzone, "dragleave", |event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
let drag_data = event
|
|
||||||
.data_transfer()
|
|
||||||
.unwrap()
|
|
||||||
.get_data("text/plain")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if drag_data.starts_with("group") {
|
|
||||||
log!("drag leave");
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.set_class_name("dropzone-group active");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(dropzone, "drop", |event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
let drag_data = event
|
|
||||||
.data_transfer()
|
|
||||||
.unwrap()
|
|
||||||
.get_data("text/plain")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if drag_data.starts_with("group") {
|
|
||||||
let id: i64 = drag_data[6..].parse().unwrap();
|
|
||||||
let target: Element = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
let group: Element = by_id(&format!("group-{}", id));
|
|
||||||
let group_dropzone: Element = by_id(&format!("dropzone-group-{}", id));
|
|
||||||
target.after_with_node_1(&group).unwrap();
|
|
||||||
group.after_with_node_1(&group_dropzone).unwrap();
|
|
||||||
|
|
||||||
send_groups_order();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_groups_order() {
|
|
||||||
spawn_local(async move {
|
|
||||||
let group_ids = by_id::<Element>("groups-container")
|
|
||||||
.selector_all::<Element>(".group")
|
|
||||||
.into_iter()
|
|
||||||
.map(|e| e.get_attribute("id").unwrap()[6..].parse::<i64>().unwrap())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let body = ron_api::SetGroupOrders { group_ids };
|
|
||||||
let _ = request::put::<(), _>("recipe/set_groups_order", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_tag_elements<T>(recipe_id: i64, tags: &[T])
|
|
||||||
where
|
|
||||||
T: AsRef<str>,
|
|
||||||
{
|
|
||||||
let tags_span: Element = selector("#container-tags .tags");
|
|
||||||
|
|
||||||
// Collect current tags to avoid re-adding an existing tag.
|
|
||||||
let mut current_tags: Vec<String> = vec![];
|
|
||||||
let mut current_tag_element = tags_span.first_child();
|
|
||||||
while let Some(element) = current_tag_element {
|
|
||||||
current_tags.push(
|
|
||||||
element
|
|
||||||
.dyn_ref::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.text_content()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
current_tag_element = element.next_sibling();
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag in tags {
|
|
||||||
let tag = tag.as_ref().to_string();
|
|
||||||
if current_tags.contains(&tag) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let tag_span = document().create_element("span").unwrap();
|
|
||||||
tag_span.set_inner_html(&tag);
|
|
||||||
let delete_tag_button: HtmlInputElement = document()
|
|
||||||
.create_element("input")
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into()
|
|
||||||
.unwrap();
|
|
||||||
delete_tag_button.set_attribute("type", "button").unwrap();
|
|
||||||
delete_tag_button.set_attribute("value", "X").unwrap();
|
|
||||||
tag_span.append_child(&delete_tag_button).unwrap();
|
|
||||||
tags_span.append_child(&tag_span).unwrap();
|
|
||||||
|
|
||||||
EventListener::new(&delete_tag_button, "click", move |_event| {
|
|
||||||
let tag_span = tag_span.clone();
|
|
||||||
let tag = tag.clone();
|
|
||||||
spawn_local(async move {
|
|
||||||
let body = ron_api::Tags {
|
|
||||||
recipe_id,
|
|
||||||
tags: vec![tag],
|
|
||||||
};
|
|
||||||
let _ = request::delete::<(), _>("recipe/rm_tags", body).await;
|
|
||||||
tag_span.remove();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_group_element(group: &ron_api::Group) -> Element {
|
|
||||||
let group_id = group.id;
|
|
||||||
let group_element: Element = selector_and_clone("#hidden-templates .group");
|
|
||||||
group_element
|
|
||||||
.set_attribute("id", &format!("group-{}", group.id))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let groups_container: Element = by_id("groups-container");
|
|
||||||
groups_container.append_child(&group_element).unwrap();
|
|
||||||
|
|
||||||
let dropzone_group: Element = selector_and_clone(".dropzone-group");
|
|
||||||
dropzone_group
|
|
||||||
.set_attribute("id", &format!("dropzone-group-{}", group.id))
|
|
||||||
.unwrap();
|
|
||||||
groups_container.append_child(&dropzone_group).unwrap();
|
|
||||||
setup_dragzone_events(&dropzone_group);
|
|
||||||
|
|
||||||
let drag_handle: Element = group_element.selector(".drag-handle");
|
|
||||||
EventListener::new(&drag_handle, "mousedown", |event| {
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.parent_element()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "true")
|
|
||||||
.unwrap();
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(&drag_handle, "mouseup", |event| {
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.parent_element()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "false")
|
|
||||||
.unwrap();
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(&group_element, "dragstart", |event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
if target_element.get_attribute("class").unwrap() == "group" {
|
|
||||||
// Highlight where the group can be droppped.
|
|
||||||
for dp in selector_all::<HtmlDivElement>(".dropzone-group") {
|
|
||||||
dp.set_class_name("dropzone-group active");
|
|
||||||
}
|
|
||||||
event
|
|
||||||
.data_transfer()
|
|
||||||
.unwrap()
|
|
||||||
.set_data("text/plain", &target_element.get_attribute("id").unwrap())
|
|
||||||
.unwrap();
|
|
||||||
event.data_transfer().unwrap().set_effect_allowed("move");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(&group_element, "dragend", |event| {
|
|
||||||
// let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "false")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
if target_element.get_attribute("class").unwrap() == "group" {
|
|
||||||
for dp in selector_all::<HtmlDivElement>(".dropzone-group") {
|
|
||||||
dp.set_class_name("dropzone-group");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Group name.
|
|
||||||
let name = group_element.selector::<HtmlInputElement>(".input-group-name");
|
|
||||||
name.set_value(&group.name);
|
|
||||||
let mut current_name = group.name.clone();
|
|
||||||
EventListener::new(&name.clone(), "blur", move |_event| {
|
|
||||||
if name.value() != current_name {
|
|
||||||
current_name = name.value();
|
|
||||||
let body = ron_api::SetGroupName {
|
|
||||||
group_id,
|
|
||||||
name: name.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_group_name", body).await;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Group comment.
|
|
||||||
let comment: HtmlInputElement = group_element.selector(".input-group-comment");
|
|
||||||
comment.set_value(&group.comment);
|
|
||||||
let mut current_comment = group.comment.clone();
|
|
||||||
EventListener::new(&comment.clone(), "blur", move |_event| {
|
|
||||||
if comment.value() != current_comment {
|
|
||||||
current_comment = comment.value();
|
|
||||||
let body = ron_api::SetGroupComment {
|
|
||||||
group_id,
|
|
||||||
comment: comment.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_group_comment", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Delete button.
|
|
||||||
let group_element_cloned = group_element.clone();
|
|
||||||
let delete_button: HtmlInputElement = group_element.selector(".input-group-delete");
|
|
||||||
EventListener::new(&delete_button, "click", move |_event| {
|
|
||||||
let name = group_element_cloned
|
|
||||||
.selector::<HtmlInputElement>(".input-group-name")
|
|
||||||
.value();
|
|
||||||
spawn_local(async move {
|
|
||||||
if modal_dialog::show(&format!("Are you sure to delete the group '{}'", name)).await
|
|
||||||
{
|
|
||||||
let body = ron_api::RemoveRecipeGroup { group_id };
|
|
||||||
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
|
||||||
by_id::<Element>(&format!("group-{}", group_id)).remove();
|
|
||||||
by_id::<Element>(&format!("dropzone-group-{}", group_id)).remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Add step button.
|
|
||||||
let add_step_button: HtmlInputElement = group_element.selector(".input-add-step");
|
|
||||||
EventListener::new(&add_step_button, "click", move |_event| {
|
|
||||||
spawn_local(async move {
|
|
||||||
let body = ron_api::AddRecipeStep { group_id };
|
|
||||||
let response: ron_api::AddRecipeStepResult =
|
|
||||||
request::post("recipe/add_step", body).await.unwrap();
|
|
||||||
create_step_element(
|
|
||||||
&selector::<Element>(&format!("#group-{} .steps", group_id)),
|
|
||||||
&ron_api::Step {
|
|
||||||
id: response.step_id,
|
|
||||||
action: "".to_string(),
|
|
||||||
ingredients: vec![],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
group_element
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element {
|
|
||||||
let step_id = step.id;
|
|
||||||
let step_element: Element = selector_and_clone("#hidden-templates .step");
|
|
||||||
step_element
|
|
||||||
.set_attribute("id", &format!("step-{}", step.id))
|
|
||||||
.unwrap();
|
|
||||||
group_element.append_child(&step_element).unwrap();
|
|
||||||
|
|
||||||
let dropzone_step: Element = selector_and_clone(".dropzone-step");
|
|
||||||
dropzone_step
|
|
||||||
.set_attribute("id", &format!("dropzone-step-{}", step.id))
|
|
||||||
.unwrap();
|
|
||||||
group_element.append_child(&dropzone_step).unwrap();
|
|
||||||
|
|
||||||
let drag_handle: Element = step_element.selector(".drag-handle");
|
|
||||||
|
|
||||||
EventListener::new(&drag_handle, "mousedown", |event| {
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.parent_element()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "true")
|
|
||||||
.unwrap();
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(&drag_handle, "mouseup", |event| {
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.parent_element()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "false")
|
|
||||||
.unwrap();
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
EventListener::new(&step_element, "dragstart", |event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
// let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
// if target_element.get_attribute("class").unwrap() == "step" {
|
|
||||||
// Highlight where the step can be droppped.
|
|
||||||
log!("START DRAG STEP");
|
|
||||||
// log!(event);
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
EventListener::new(&step_element, "dragend", |event| {
|
|
||||||
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
|
||||||
// let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
|
||||||
// if target_element.get_attribute("class").unwrap() == "step" {
|
|
||||||
// Highlight where the step can be droppped.
|
|
||||||
event
|
|
||||||
.target()
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into::<Element>()
|
|
||||||
.unwrap()
|
|
||||||
.set_attribute("draggable", "false")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
log!("STOP DRAG STEP");
|
|
||||||
// log!(event);
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Step action.
|
|
||||||
let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action");
|
|
||||||
action.set_value(&step.action);
|
|
||||||
let mut current_action = step.action.clone();
|
|
||||||
EventListener::new(&action.clone(), "blur", move |_event| {
|
|
||||||
if action.value() != current_action {
|
|
||||||
current_action = action.value();
|
|
||||||
let body = ron_api::SetStepAction {
|
|
||||||
step_id,
|
|
||||||
action: action.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_step_action", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Delete button.
|
|
||||||
let step_element_cloned = step_element.clone();
|
|
||||||
let delete_button: HtmlInputElement = step_element.selector(".input-step-delete");
|
|
||||||
EventListener::new(&delete_button, "click", move |_event| {
|
|
||||||
let action = step_element_cloned
|
|
||||||
.selector::<HtmlTextAreaElement>(".text-area-step-action")
|
|
||||||
.value();
|
|
||||||
spawn_local(async move {
|
|
||||||
if modal_dialog::show(&format!("Are you sure to delete the step '{}'", action))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let body = ron_api::RemoveRecipeStep { step_id };
|
|
||||||
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
|
||||||
by_id::<Element>(&format!("step-{}", step_id)).remove();
|
|
||||||
by_id::<Element>(&format!("dropzone-step-{}", step_id)).remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Add ingredient button.
|
|
||||||
let add_ingredient_button: HtmlInputElement =
|
|
||||||
step_element.selector(".input-add-ingredient");
|
|
||||||
EventListener::new(&add_ingredient_button, "click", move |_event| {
|
|
||||||
spawn_local(async move {
|
|
||||||
let body = ron_api::AddRecipeIngredient { step_id };
|
|
||||||
let response: ron_api::AddRecipeIngredientResult =
|
|
||||||
request::post("recipe/add_ingredient", body).await.unwrap();
|
|
||||||
create_ingredient_element(
|
|
||||||
&selector::<Element>(&format!("#step-{} .ingredients", step_id)),
|
|
||||||
&ron_api::Ingredient {
|
|
||||||
id: response.ingredient_id,
|
|
||||||
name: "".to_string(),
|
|
||||||
comment: "".to_string(),
|
|
||||||
quantity_value: None,
|
|
||||||
quantity_unit: "".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
step_element
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_ingredient_element(
|
|
||||||
step_element: &Element,
|
|
||||||
ingredient: &ron_api::Ingredient,
|
|
||||||
) -> Element {
|
|
||||||
let ingredient_id = ingredient.id;
|
|
||||||
let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient");
|
|
||||||
ingredient_element
|
|
||||||
.set_attribute("id", &format!("ingredient-{}", ingredient.id))
|
|
||||||
.unwrap();
|
|
||||||
step_element.append_child(&ingredient_element).unwrap();
|
|
||||||
|
|
||||||
// Ingredient name.
|
|
||||||
let name: HtmlInputElement = ingredient_element.selector(".input-ingredient-name");
|
|
||||||
name.set_value(&ingredient.name);
|
|
||||||
let mut current_name = ingredient.name.clone();
|
|
||||||
EventListener::new(&name.clone(), "blur", move |_event| {
|
|
||||||
if name.value() != current_name {
|
|
||||||
current_name = name.value();
|
|
||||||
let body = ron_api::SetIngredientName {
|
|
||||||
ingredient_id,
|
|
||||||
name: name.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_ingredient_name", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Ingredient comment.
|
|
||||||
let comment: HtmlInputElement = ingredient_element.selector(".input-ingredient-comment");
|
|
||||||
comment.set_value(&ingredient.comment);
|
|
||||||
let mut current_comment = ingredient.comment.clone();
|
|
||||||
EventListener::new(&comment.clone(), "blur", move |_event| {
|
|
||||||
if comment.value() != current_comment {
|
|
||||||
current_comment = comment.value();
|
|
||||||
let body = ron_api::SetIngredientComment {
|
|
||||||
ingredient_id,
|
|
||||||
comment: comment.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_ingredient_comment", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Ingredient quantity.
|
|
||||||
let quantity: HtmlInputElement = ingredient_element.selector(".input-ingredient-quantity");
|
|
||||||
quantity.set_value(
|
|
||||||
&ingredient
|
|
||||||
.quantity_value
|
|
||||||
.map_or("".to_string(), |q| q.to_string()),
|
|
||||||
);
|
|
||||||
let mut current_quantity = quantity.value_as_number();
|
|
||||||
EventListener::new(&quantity.clone(), "input", move |_event| {
|
|
||||||
let n = quantity.value_as_number();
|
|
||||||
if n.is_nan() {
|
|
||||||
quantity.set_value("");
|
|
||||||
}
|
|
||||||
if n != current_quantity {
|
|
||||||
let q = if n.is_nan() { None } else { Some(n) };
|
|
||||||
current_quantity = n;
|
|
||||||
let body = ron_api::SetIngredientQuantity {
|
|
||||||
ingredient_id,
|
|
||||||
quantity: q,
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_ingredient_quantity", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Ingredient unit.
|
|
||||||
let unit: HtmlInputElement = ingredient_element.selector(".input-ingredient-unit");
|
|
||||||
unit.set_value(&ingredient.quantity_unit);
|
|
||||||
let mut current_unit = ingredient.quantity_unit.clone();
|
|
||||||
EventListener::new(&unit.clone(), "blur", move |_event| {
|
|
||||||
if unit.value() != current_unit {
|
|
||||||
current_unit = unit.value();
|
|
||||||
let body = ron_api::SetIngredientUnit {
|
|
||||||
ingredient_id,
|
|
||||||
unit: unit.value(),
|
|
||||||
};
|
|
||||||
spawn_local(async move {
|
|
||||||
let _ = request::put::<(), _>("recipe/set_ingredient_unit", body).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
// Delete button.
|
|
||||||
let ingredient_element_cloned = ingredient_element.clone();
|
|
||||||
let delete_button: HtmlInputElement =
|
|
||||||
ingredient_element.selector(".input-ingredient-delete");
|
|
||||||
EventListener::new(&delete_button, "click", move |_event| {
|
|
||||||
let name = ingredient_element_cloned
|
|
||||||
.selector::<HtmlInputElement>(".input-ingredient-name")
|
|
||||||
.value();
|
|
||||||
spawn_local(async move {
|
|
||||||
if modal_dialog::show(&format!("Are you sure to delete the ingredient '{}'", name))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let body = ron_api::RemoveRecipeIngredient { ingredient_id };
|
|
||||||
let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await;
|
|
||||||
by_id::<Element>(&format!("ingredient-{}", ingredient_id)).remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.forget();
|
|
||||||
|
|
||||||
ingredient_element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load initial groups, steps and ingredients.
|
|
||||||
{
|
|
||||||
spawn_local(async move {
|
|
||||||
let groups: Vec<common::ron_api::Group> =
|
|
||||||
request::get("recipe/get_groups", [("recipe_id", &recipe_id.to_string())])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for group in groups {
|
|
||||||
let group_element = create_group_element(&group);
|
|
||||||
|
|
||||||
for step in group.steps {
|
|
||||||
let step_element =
|
|
||||||
create_step_element(&group_element.selector(".steps"), &step);
|
|
||||||
|
|
||||||
for ingredient in step.ingredients {
|
|
||||||
create_ingredient_element(
|
|
||||||
&step_element.selector(".ingredients"),
|
|
||||||
&ingredient,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new group.
|
|
||||||
{
|
|
||||||
let button_add_group: HtmlInputElement = by_id("input-add-group");
|
|
||||||
let on_click_add_group = EventListener::new(&button_add_group, "click", move |_event| {
|
|
||||||
let body = ron_api::AddRecipeGroup { recipe_id };
|
|
||||||
spawn_local(async move {
|
|
||||||
let response: ron_api::AddRecipeGroupResult =
|
|
||||||
request::post("recipe/add_group", body).await.unwrap();
|
|
||||||
create_group_element(&ron_api::Group {
|
|
||||||
id: response.group_id,
|
|
||||||
name: "".to_string(),
|
|
||||||
comment: "".to_string(),
|
|
||||||
steps: vec![],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
on_click_add_group.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
mod handles;
|
|
||||||
mod modal_dialog;
|
mod modal_dialog;
|
||||||
mod on_click;
|
mod on_click;
|
||||||
|
mod recipe_edit;
|
||||||
mod request;
|
mod request;
|
||||||
mod toast;
|
mod toast;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
@ -22,7 +22,7 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
|
|
||||||
if let ["recipe", "edit", id] = path[..] {
|
if let ["recipe", "edit", id] = path[..] {
|
||||||
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
||||||
if let Err(error) = handles::recipe_edit(id) {
|
if let Err(error) = recipe_edit::setup_page(id) {
|
||||||
log!(error);
|
log!(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
849
frontend/src/recipe_edit.rs
Normal file
849
frontend/src/recipe_edit.rs
Normal file
|
|
@ -0,0 +1,849 @@
|
||||||
|
use std::rc;
|
||||||
|
|
||||||
|
use gloo::{
|
||||||
|
console::log,
|
||||||
|
events::{EventListener, EventListenerOptions},
|
||||||
|
net::http::Request,
|
||||||
|
utils::{document, window},
|
||||||
|
};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use web_sys::{
|
||||||
|
DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
||||||
|
KeyboardEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
use common::ron_api;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
modal_dialog, request,
|
||||||
|
toast::{self, Level},
|
||||||
|
utils::{by_id, selector, selector_all, selector_and_clone, SelectorExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn reload_recipes_list(current_recipe_id: i64) {
|
||||||
|
match Request::get("/fragments/recipes_list")
|
||||||
|
.query([("current_recipe_id", current_recipe_id.to_string())])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Err(error) => {
|
||||||
|
toast::show(Level::Info, &format!("Internal server error: {}", error));
|
||||||
|
}
|
||||||
|
Ok(response) => {
|
||||||
|
let list = document().get_element_by_id("recipes-list").unwrap();
|
||||||
|
list.set_outer_html(&response.text().await.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_page(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
|
// Title.
|
||||||
|
{
|
||||||
|
let Some(title) = document().get_element_by_id("input-title") else {
|
||||||
|
return Err(JsValue::from_str("Unable to find 'input-title' element"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let title: HtmlInputElement = title.dyn_into().unwrap();
|
||||||
|
|
||||||
|
// Check if the recipe has been loaded.
|
||||||
|
|
||||||
|
let mut current_title = title.value();
|
||||||
|
EventListener::new(&title.clone(), "blur", move |_event| {
|
||||||
|
if title.value() != current_title {
|
||||||
|
current_title = title.value();
|
||||||
|
let body = ron_api::SetRecipeTitle {
|
||||||
|
recipe_id,
|
||||||
|
title: title.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_title", body).await;
|
||||||
|
reload_recipes_list(recipe_id).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description.
|
||||||
|
{
|
||||||
|
let description: HtmlTextAreaElement = by_id("text-area-description");
|
||||||
|
let mut current_description = description.value();
|
||||||
|
|
||||||
|
EventListener::new(&description.clone(), "blur", move |_event| {
|
||||||
|
if description.value() != current_description {
|
||||||
|
current_description = description.value();
|
||||||
|
let body = ron_api::SetRecipeDescription {
|
||||||
|
recipe_id,
|
||||||
|
description: description.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_description", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Servings.
|
||||||
|
{
|
||||||
|
let servings: HtmlInputElement = by_id("input-servings");
|
||||||
|
let mut current_servings = servings.value_as_number();
|
||||||
|
EventListener::new(&servings.clone(), "input", move |_event| {
|
||||||
|
let n = servings.value_as_number();
|
||||||
|
if n.is_nan() {
|
||||||
|
servings.set_value("");
|
||||||
|
}
|
||||||
|
if n != current_servings {
|
||||||
|
let servings = if n.is_nan() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// TODO: Find a better way to validate integer numbers.
|
||||||
|
let n = n as u32;
|
||||||
|
servings.set_value_as_number(n as f64);
|
||||||
|
Some(n)
|
||||||
|
};
|
||||||
|
current_servings = n;
|
||||||
|
let body = ron_api::SetRecipeServings {
|
||||||
|
recipe_id,
|
||||||
|
servings,
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_servings", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimated time.
|
||||||
|
{
|
||||||
|
let estimated_time: HtmlInputElement = by_id("input-estimated-time");
|
||||||
|
let mut current_time = estimated_time.value_as_number();
|
||||||
|
|
||||||
|
EventListener::new(&estimated_time.clone(), "input", move |_event| {
|
||||||
|
let n = estimated_time.value_as_number();
|
||||||
|
if n.is_nan() {
|
||||||
|
estimated_time.set_value("");
|
||||||
|
}
|
||||||
|
if n != current_time {
|
||||||
|
let time = if n.is_nan() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// TODO: Find a better way to validate integer numbers.
|
||||||
|
let n = n as u32;
|
||||||
|
estimated_time.set_value_as_number(n as f64);
|
||||||
|
Some(n)
|
||||||
|
};
|
||||||
|
current_time = n;
|
||||||
|
let body = ron_api::SetRecipeEstimatedTime {
|
||||||
|
recipe_id,
|
||||||
|
estimated_time: time,
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_estimated_time", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difficulty.
|
||||||
|
{
|
||||||
|
let difficulty: HtmlSelectElement = by_id("select-difficulty");
|
||||||
|
let mut current_difficulty = difficulty.value();
|
||||||
|
|
||||||
|
EventListener::new(&difficulty.clone(), "blur", move |_event| {
|
||||||
|
if difficulty.value() != current_difficulty {
|
||||||
|
current_difficulty = difficulty.value();
|
||||||
|
|
||||||
|
let body = ron_api::SetRecipeDifficulty {
|
||||||
|
recipe_id,
|
||||||
|
difficulty: ron_api::Difficulty::try_from(
|
||||||
|
current_difficulty.parse::<u32>().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_difficulty", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags.
|
||||||
|
{
|
||||||
|
spawn_local(async move {
|
||||||
|
let tags: ron_api::Tags =
|
||||||
|
request::get("recipe/get_tags", [("recipe_id", &recipe_id.to_string())])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
create_tag_elements(recipe_id, &tags.tags);
|
||||||
|
});
|
||||||
|
|
||||||
|
fn add_tags(recipe_id: i64, tags: String) {
|
||||||
|
spawn_local(async move {
|
||||||
|
let tag_list: Vec<String> = tags.split_whitespace().map(String::from).collect();
|
||||||
|
if !tag_list.is_empty() {
|
||||||
|
let body = ron_api::Tags {
|
||||||
|
recipe_id,
|
||||||
|
tags: tag_list.clone(),
|
||||||
|
};
|
||||||
|
let _ = request::post::<(), _>("recipe/add_tags", body).await;
|
||||||
|
create_tag_elements(recipe_id, &tag_list);
|
||||||
|
}
|
||||||
|
by_id::<HtmlInputElement>("input-tags").set_value("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_tags: HtmlInputElement = by_id("input-tags");
|
||||||
|
EventListener::new(&input_tags.clone(), "input", move |_event| {
|
||||||
|
let tags = input_tags.value();
|
||||||
|
if tags.ends_with(' ') {
|
||||||
|
add_tags(recipe_id, tags);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let input_tags: HtmlInputElement = by_id("input-tags");
|
||||||
|
EventListener::new(&input_tags.clone(), "keypress", move |event| {
|
||||||
|
if let Some(keyboard_event) = event.dyn_ref::<KeyboardEvent>() {
|
||||||
|
if keyboard_event.key_code() == 13 {
|
||||||
|
let tags = input_tags.value();
|
||||||
|
add_tags(recipe_id, tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let input_tags: HtmlInputElement = by_id("input-tags");
|
||||||
|
EventListener::new(&input_tags.clone(), "blur", move |_event| {
|
||||||
|
let tags = input_tags.value();
|
||||||
|
add_tags(recipe_id, tags);
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language.
|
||||||
|
{
|
||||||
|
let language: HtmlSelectElement = by_id("select-language");
|
||||||
|
let mut current_language = language.value();
|
||||||
|
EventListener::new(&language.clone(), "blur", move |_event| {
|
||||||
|
if language.value() != current_language {
|
||||||
|
current_language = language.value();
|
||||||
|
|
||||||
|
let body = ron_api::SetRecipeLanguage {
|
||||||
|
recipe_id,
|
||||||
|
lang: language.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_language", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is published.
|
||||||
|
{
|
||||||
|
let is_published: HtmlInputElement = by_id("input-is-published");
|
||||||
|
EventListener::new(&is_published.clone(), "input", move |_event| {
|
||||||
|
let body = ron_api::SetIsPublished {
|
||||||
|
recipe_id,
|
||||||
|
is_published: is_published.checked(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_is_published", body).await;
|
||||||
|
reload_recipes_list(recipe_id).await;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete recipe button.
|
||||||
|
let delete_button: HtmlInputElement = by_id("input-delete");
|
||||||
|
EventListener::new(&delete_button, "click", move |_event| {
|
||||||
|
let title: HtmlInputElement = by_id("input-title");
|
||||||
|
spawn_local(async move {
|
||||||
|
if modal_dialog::show(&format!(
|
||||||
|
"Are you sure to delete the recipe '{}'",
|
||||||
|
title.value()
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let body = ron_api::Remove { recipe_id };
|
||||||
|
let _ = request::delete::<(), _>("recipe/remove", body).await;
|
||||||
|
window().location().set_href("/").unwrap();
|
||||||
|
|
||||||
|
// by_id::<Element>(&format!("group-{}", group_id)).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// let group_dropzone: Element = selector(".dropzone-group");
|
||||||
|
// setup_dragzone_events(&group_dropzone);
|
||||||
|
|
||||||
|
// Load initial groups, steps and ingredients.
|
||||||
|
{
|
||||||
|
spawn_local(async move {
|
||||||
|
let groups: Vec<common::ron_api::Group> =
|
||||||
|
request::get("recipe/get_groups", [("recipe_id", &recipe_id.to_string())])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for group in groups {
|
||||||
|
let group_element = create_group_element(&group);
|
||||||
|
|
||||||
|
for step in group.steps {
|
||||||
|
let step_element =
|
||||||
|
create_step_element(&group_element.selector(".steps"), &step);
|
||||||
|
|
||||||
|
for ingredient in step.ingredients {
|
||||||
|
create_ingredient_element(
|
||||||
|
&step_element.selector(".ingredients"),
|
||||||
|
&ingredient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new group.
|
||||||
|
{
|
||||||
|
let button_add_group: HtmlInputElement = by_id("input-add-group");
|
||||||
|
let on_click_add_group = EventListener::new(&button_add_group, "click", move |_event| {
|
||||||
|
let body = ron_api::AddRecipeGroup { recipe_id };
|
||||||
|
spawn_local(async move {
|
||||||
|
let response: ron_api::AddRecipeGroupResult =
|
||||||
|
request::post("recipe/add_group", body).await.unwrap();
|
||||||
|
create_group_element(&ron_api::Group {
|
||||||
|
id: response.group_id,
|
||||||
|
name: "".to_string(),
|
||||||
|
comment: "".to_string(),
|
||||||
|
steps: vec![],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
on_click_add_group.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_group_element(group: &ron_api::Group) -> Element {
|
||||||
|
let group_id = group.id;
|
||||||
|
let group_element: Element = selector_and_clone("#hidden-templates .group");
|
||||||
|
group_element
|
||||||
|
.set_attribute("id", &format!("group-{}", group.id))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let groups_container: Element = by_id("groups-container");
|
||||||
|
groups_container.append_child(&group_element).unwrap();
|
||||||
|
|
||||||
|
set_draggable(&group_element, "group", |_element| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let group_ids = by_id::<Element>("groups-container")
|
||||||
|
.selector_all::<Element>(".group")
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.get_attribute("id").unwrap()[6..].parse::<i64>().unwrap())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let body = ron_api::SetGroupOrders { group_ids };
|
||||||
|
let _ = request::put::<(), _>("recipe/set_groups_order", body).await;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group name.
|
||||||
|
let name = group_element.selector::<HtmlInputElement>(".input-group-name");
|
||||||
|
name.set_value(&group.name);
|
||||||
|
let mut current_name = group.name.clone();
|
||||||
|
EventListener::new(&name.clone(), "blur", move |_event| {
|
||||||
|
if name.value() != current_name {
|
||||||
|
current_name = name.value();
|
||||||
|
let body = ron_api::SetGroupName {
|
||||||
|
group_id,
|
||||||
|
name: name.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_group_name", body).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Group comment.
|
||||||
|
let comment: HtmlInputElement = group_element.selector(".input-group-comment");
|
||||||
|
comment.set_value(&group.comment);
|
||||||
|
let mut current_comment = group.comment.clone();
|
||||||
|
EventListener::new(&comment.clone(), "blur", move |_event| {
|
||||||
|
if comment.value() != current_comment {
|
||||||
|
current_comment = comment.value();
|
||||||
|
let body = ron_api::SetGroupComment {
|
||||||
|
group_id,
|
||||||
|
comment: comment.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_group_comment", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Delete button.
|
||||||
|
let group_element_cloned = group_element.clone();
|
||||||
|
let delete_button: HtmlInputElement = group_element.selector(".input-group-delete");
|
||||||
|
EventListener::new(&delete_button, "click", move |_event| {
|
||||||
|
let name = group_element_cloned
|
||||||
|
.selector::<HtmlInputElement>(".input-group-name")
|
||||||
|
.value();
|
||||||
|
spawn_local(async move {
|
||||||
|
if modal_dialog::show(&format!("Are you sure to delete the group '{}'", name)).await {
|
||||||
|
let body = ron_api::RemoveRecipeGroup { group_id };
|
||||||
|
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
||||||
|
by_id::<Element>(&format!("group-{}", group_id)).remove();
|
||||||
|
by_id::<Element>(&format!("dropzone-group-{}", group_id)).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Add step button.
|
||||||
|
let add_step_button: HtmlInputElement = group_element.selector(".input-add-step");
|
||||||
|
EventListener::new(&add_step_button, "click", move |_event| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let body = ron_api::AddRecipeStep { group_id };
|
||||||
|
let response: ron_api::AddRecipeStepResult =
|
||||||
|
request::post("recipe/add_step", body).await.unwrap();
|
||||||
|
create_step_element(
|
||||||
|
&selector::<Element>(&format!("#group-{} .steps", group_id)),
|
||||||
|
&ron_api::Step {
|
||||||
|
id: response.step_id,
|
||||||
|
action: "".to_string(),
|
||||||
|
ingredients: vec![],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
group_element
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_tag_elements<T>(recipe_id: i64, tags: &[T])
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
let tags_span: Element = selector("#container-tags .tags");
|
||||||
|
|
||||||
|
// Collect current tags to avoid re-adding an existing tag.
|
||||||
|
let mut current_tags: Vec<String> = vec![];
|
||||||
|
let mut current_tag_element = tags_span.first_child();
|
||||||
|
while let Some(element) = current_tag_element {
|
||||||
|
current_tags.push(
|
||||||
|
element
|
||||||
|
.dyn_ref::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.text_content()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
current_tag_element = element.next_sibling();
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag in tags {
|
||||||
|
let tag = tag.as_ref().to_string();
|
||||||
|
if current_tags.contains(&tag) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tag_span = document().create_element("span").unwrap();
|
||||||
|
tag_span.set_inner_html(&tag);
|
||||||
|
let delete_tag_button: HtmlInputElement = document()
|
||||||
|
.create_element("input")
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
|
delete_tag_button.set_attribute("type", "button").unwrap();
|
||||||
|
delete_tag_button.set_attribute("value", "X").unwrap();
|
||||||
|
tag_span.append_child(&delete_tag_button).unwrap();
|
||||||
|
tags_span.append_child(&tag_span).unwrap();
|
||||||
|
|
||||||
|
EventListener::new(&delete_tag_button, "click", move |_event| {
|
||||||
|
let tag_span = tag_span.clone();
|
||||||
|
let tag = tag.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
let body = ron_api::Tags {
|
||||||
|
recipe_id,
|
||||||
|
tags: vec![tag],
|
||||||
|
};
|
||||||
|
let _ = request::delete::<(), _>("recipe/rm_tags", body).await;
|
||||||
|
tag_span.remove();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element {
|
||||||
|
let step_id = step.id;
|
||||||
|
let step_element: Element = selector_and_clone("#hidden-templates .step");
|
||||||
|
step_element
|
||||||
|
.set_attribute("id", &format!("step-{}", step.id))
|
||||||
|
.unwrap();
|
||||||
|
group_element.append_child(&step_element).unwrap();
|
||||||
|
|
||||||
|
// Step action.
|
||||||
|
let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action");
|
||||||
|
action.set_value(&step.action);
|
||||||
|
let mut current_action = step.action.clone();
|
||||||
|
EventListener::new(&action.clone(), "blur", move |_event| {
|
||||||
|
if action.value() != current_action {
|
||||||
|
current_action = action.value();
|
||||||
|
let body = ron_api::SetStepAction {
|
||||||
|
step_id,
|
||||||
|
action: action.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_step_action", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Delete button.
|
||||||
|
let step_element_cloned = step_element.clone();
|
||||||
|
let delete_button: HtmlInputElement = step_element.selector(".input-step-delete");
|
||||||
|
EventListener::new(&delete_button, "click", move |_event| {
|
||||||
|
let action = step_element_cloned
|
||||||
|
.selector::<HtmlTextAreaElement>(".text-area-step-action")
|
||||||
|
.value();
|
||||||
|
spawn_local(async move {
|
||||||
|
if modal_dialog::show(&format!("Are you sure to delete the step '{}'", action)).await {
|
||||||
|
let body = ron_api::RemoveRecipeStep { step_id };
|
||||||
|
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
||||||
|
by_id::<Element>(&format!("step-{}", step_id)).remove();
|
||||||
|
by_id::<Element>(&format!("dropzone-step-{}", step_id)).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Add ingredient button.
|
||||||
|
let add_ingredient_button: HtmlInputElement = step_element.selector(".input-add-ingredient");
|
||||||
|
EventListener::new(&add_ingredient_button, "click", move |_event| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let body = ron_api::AddRecipeIngredient { step_id };
|
||||||
|
let response: ron_api::AddRecipeIngredientResult =
|
||||||
|
request::post("recipe/add_ingredient", body).await.unwrap();
|
||||||
|
create_ingredient_element(
|
||||||
|
&selector::<Element>(&format!("#step-{} .ingredients", step_id)),
|
||||||
|
&ron_api::Ingredient {
|
||||||
|
id: response.ingredient_id,
|
||||||
|
name: "".to_string(),
|
||||||
|
comment: "".to_string(),
|
||||||
|
quantity_value: None,
|
||||||
|
quantity_unit: "".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
step_element
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingredient) -> Element {
|
||||||
|
let ingredient_id = ingredient.id;
|
||||||
|
let ingredient_element: Element = selector_and_clone("#hidden-templates .ingredient");
|
||||||
|
ingredient_element
|
||||||
|
.set_attribute("id", &format!("ingredient-{}", ingredient.id))
|
||||||
|
.unwrap();
|
||||||
|
step_element.append_child(&ingredient_element).unwrap();
|
||||||
|
|
||||||
|
// Ingredient name.
|
||||||
|
let name: HtmlInputElement = ingredient_element.selector(".input-ingredient-name");
|
||||||
|
name.set_value(&ingredient.name);
|
||||||
|
let mut current_name = ingredient.name.clone();
|
||||||
|
EventListener::new(&name.clone(), "blur", move |_event| {
|
||||||
|
if name.value() != current_name {
|
||||||
|
current_name = name.value();
|
||||||
|
let body = ron_api::SetIngredientName {
|
||||||
|
ingredient_id,
|
||||||
|
name: name.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_ingredient_name", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Ingredient comment.
|
||||||
|
let comment: HtmlInputElement = ingredient_element.selector(".input-ingredient-comment");
|
||||||
|
comment.set_value(&ingredient.comment);
|
||||||
|
let mut current_comment = ingredient.comment.clone();
|
||||||
|
EventListener::new(&comment.clone(), "blur", move |_event| {
|
||||||
|
if comment.value() != current_comment {
|
||||||
|
current_comment = comment.value();
|
||||||
|
let body = ron_api::SetIngredientComment {
|
||||||
|
ingredient_id,
|
||||||
|
comment: comment.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_ingredient_comment", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Ingredient quantity.
|
||||||
|
let quantity: HtmlInputElement = ingredient_element.selector(".input-ingredient-quantity");
|
||||||
|
quantity.set_value(
|
||||||
|
&ingredient
|
||||||
|
.quantity_value
|
||||||
|
.map_or("".to_string(), |q| q.to_string()),
|
||||||
|
);
|
||||||
|
let mut current_quantity = quantity.value_as_number();
|
||||||
|
EventListener::new(&quantity.clone(), "input", move |_event| {
|
||||||
|
let n = quantity.value_as_number();
|
||||||
|
if n.is_nan() {
|
||||||
|
quantity.set_value("");
|
||||||
|
}
|
||||||
|
if n != current_quantity {
|
||||||
|
let q = if n.is_nan() { None } else { Some(n) };
|
||||||
|
current_quantity = n;
|
||||||
|
let body = ron_api::SetIngredientQuantity {
|
||||||
|
ingredient_id,
|
||||||
|
quantity: q,
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_ingredient_quantity", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Ingredient unit.
|
||||||
|
let unit: HtmlInputElement = ingredient_element.selector(".input-ingredient-unit");
|
||||||
|
unit.set_value(&ingredient.quantity_unit);
|
||||||
|
let mut current_unit = ingredient.quantity_unit.clone();
|
||||||
|
EventListener::new(&unit.clone(), "blur", move |_event| {
|
||||||
|
if unit.value() != current_unit {
|
||||||
|
current_unit = unit.value();
|
||||||
|
let body = ron_api::SetIngredientUnit {
|
||||||
|
ingredient_id,
|
||||||
|
unit: unit.value(),
|
||||||
|
};
|
||||||
|
spawn_local(async move {
|
||||||
|
let _ = request::put::<(), _>("recipe/set_ingredient_unit", body).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
// Delete button.
|
||||||
|
let ingredient_element_cloned = ingredient_element.clone();
|
||||||
|
let delete_button: HtmlInputElement = ingredient_element.selector(".input-ingredient-delete");
|
||||||
|
EventListener::new(&delete_button, "click", move |_event| {
|
||||||
|
let name = ingredient_element_cloned
|
||||||
|
.selector::<HtmlInputElement>(".input-ingredient-name")
|
||||||
|
.value();
|
||||||
|
spawn_local(async move {
|
||||||
|
if modal_dialog::show(&format!("Are you sure to delete the ingredient '{}'", name))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let body = ron_api::RemoveRecipeIngredient { ingredient_id };
|
||||||
|
let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await;
|
||||||
|
by_id::<Element>(&format!("ingredient-{}", ingredient_id)).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
ingredient_element
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set an element as draggable and add an element before and after
|
||||||
|
/// cloned from "#hidden-templates .dropzone".
|
||||||
|
/// All elements set as draggable in a given container can be dragged
|
||||||
|
/// After or before another element.
|
||||||
|
/// 'element' must have a sub-element with the class '.drag-handle' which
|
||||||
|
/// will be used to drag the element.
|
||||||
|
fn set_draggable<T>(element: &Element, prefix: &str, dropped: T)
|
||||||
|
where
|
||||||
|
T: Fn(&Element) + 'static,
|
||||||
|
{
|
||||||
|
let dropped = rc::Rc::new(dropped);
|
||||||
|
|
||||||
|
// Add a drop zone before the given element if there is none.
|
||||||
|
if element.previous_element_sibling().is_none() {
|
||||||
|
let dropzone = selector_and_clone::<Element>("#hidden-templates .dropzone");
|
||||||
|
element.before_with_node_1(&dropzone).unwrap();
|
||||||
|
setup_dragzone_events(&dropzone, prefix, dropped.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropzone = selector_and_clone::<Element>("#hidden-templates .dropzone");
|
||||||
|
element.after_with_node_1(&dropzone).unwrap();
|
||||||
|
setup_dragzone_events(&dropzone, prefix, dropped.clone());
|
||||||
|
|
||||||
|
let drag_handle: Element = element.selector(".drag-handle");
|
||||||
|
EventListener::new(&drag_handle, "mousedown", |event| {
|
||||||
|
event
|
||||||
|
.target()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.set_attribute("draggable", "true")
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
EventListener::new(&drag_handle, "mouseup", |event| {
|
||||||
|
event
|
||||||
|
.target()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.set_attribute("draggable", "false")
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let prefix_copied = prefix.to_string();
|
||||||
|
EventListener::new(element, "dragstart", move |event| {
|
||||||
|
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
||||||
|
let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
||||||
|
|
||||||
|
if target_element
|
||||||
|
.get_attribute("id")
|
||||||
|
.unwrap()
|
||||||
|
.starts_with(&prefix_copied)
|
||||||
|
{
|
||||||
|
// Highlight where the group can be droppped.
|
||||||
|
// TODO: only select direct children.
|
||||||
|
for dp in target_element
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.selector_all::<HtmlDivElement>(".dropzone")
|
||||||
|
{
|
||||||
|
dp.set_class_name("dropzone active");
|
||||||
|
}
|
||||||
|
event
|
||||||
|
.data_transfer()
|
||||||
|
.unwrap()
|
||||||
|
.set_data("text/plain", &target_element.get_attribute("id").unwrap())
|
||||||
|
.unwrap();
|
||||||
|
event.data_transfer().unwrap().set_effect_allowed("move");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let prefix_copied = prefix.to_string();
|
||||||
|
EventListener::new(element, "dragend", move |event| {
|
||||||
|
// let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
||||||
|
event
|
||||||
|
.target()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.set_attribute("draggable", "false")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let target_element: Element = event.target().unwrap().dyn_into().unwrap();
|
||||||
|
if target_element
|
||||||
|
.get_attribute("id")
|
||||||
|
.unwrap()
|
||||||
|
.starts_with(&prefix_copied)
|
||||||
|
{
|
||||||
|
for dp in target_element
|
||||||
|
.parent_element()
|
||||||
|
.unwrap()
|
||||||
|
.selector_all::<HtmlDivElement>(".dropzone")
|
||||||
|
{
|
||||||
|
dp.set_class_name("dropzone");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_dragzone_events<T>(dropzone: &Element, prefix: &str, dropped: rc::Rc<T>)
|
||||||
|
where
|
||||||
|
T: Fn(&Element) + 'static,
|
||||||
|
{
|
||||||
|
let prefix_copied = prefix.to_string();
|
||||||
|
EventListener::new_with_options(
|
||||||
|
dropzone,
|
||||||
|
"dragover",
|
||||||
|
EventListenerOptions::enable_prevent_default(),
|
||||||
|
move |event| {
|
||||||
|
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
||||||
|
let drag_data = event
|
||||||
|
.data_transfer()
|
||||||
|
.unwrap()
|
||||||
|
.get_data("text/plain")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if drag_data.starts_with(&prefix_copied) {
|
||||||
|
event.prevent_default();
|
||||||
|
// event.data_transfer().unwrap().set_effect_allowed("move");
|
||||||
|
// log!("drag over");
|
||||||
|
event
|
||||||
|
.target()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.set_class_name("dropzone hover");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let prefix_copied = prefix.to_string();
|
||||||
|
EventListener::new(dropzone, "dragleave", move |event| {
|
||||||
|
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
||||||
|
let drag_data = event
|
||||||
|
.data_transfer()
|
||||||
|
.unwrap()
|
||||||
|
.get_data("text/plain")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if drag_data.starts_with(&prefix_copied) {
|
||||||
|
// log!("drag leave");
|
||||||
|
event
|
||||||
|
.target()
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into::<Element>()
|
||||||
|
.unwrap()
|
||||||
|
.set_class_name("dropzone active");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
|
||||||
|
let prefix_copied = prefix.to_string();
|
||||||
|
EventListener::new(dropzone, "drop", move |event| {
|
||||||
|
let event: &DragEvent = event.dyn_ref::<DragEvent>().unwrap();
|
||||||
|
let drag_data = event
|
||||||
|
.data_transfer()
|
||||||
|
.unwrap()
|
||||||
|
.get_data("text/plain")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if drag_data.starts_with(&prefix_copied) {
|
||||||
|
let id: i64 = drag_data[prefix_copied.len() + 1..].parse().unwrap();
|
||||||
|
let target: Element = event.target().unwrap().dyn_into().unwrap();
|
||||||
|
let element: Element = by_id(&format!("{}-{}", &prefix_copied, id));
|
||||||
|
let group_dropzone: Element = element.next_element_sibling().unwrap(); // = by_id(&format!("dropzone-group-{}", id));
|
||||||
|
target.after_with_node_1(&element).unwrap();
|
||||||
|
element.after_with_node_1(&group_dropzone).unwrap();
|
||||||
|
|
||||||
|
dropped(&element);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.forget();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue