Groups can now be ordered (via drag and drop)
This commit is contained in:
parent
16c484c2d1
commit
975d1ceee2
14 changed files with 461 additions and 54 deletions
48
Cargo.lock
generated
48
Cargo.lock
generated
|
|
@ -290,9 +290,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
@ -374,9 +374,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.24"
|
version = "4.5.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd"
|
checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
|
@ -384,9 +384,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.24"
|
version = "4.5.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd"
|
checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
|
@ -718,7 +718,7 @@ dependencies = [
|
||||||
"gloo",
|
"gloo",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
|
@ -1934,7 +1934,7 @@ dependencies = [
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|
@ -2121,9 +2121,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.20"
|
version = "0.23.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
|
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2386,7 +2386,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2470,7 +2470,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
@ -2508,7 +2508,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.11",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
@ -2600,9 +2600,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.95"
|
version = "2.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
|
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2651,11 +2651,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
|
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.9",
|
"thiserror-impl 2.0.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2671,9 +2671,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.9"
|
version = "2.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
|
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2748,9 +2748,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.42.0"
|
version = "1.43.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
|
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -2766,9 +2766,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,8 @@ body {
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
background-color: blue;
|
background-color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +94,6 @@ body {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.recipe-item {
|
.recipe-item {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +119,13 @@ body {
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#recipe-edit {
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
border: 0.1em solid lighten($color-3, 30%);
|
border: 0.1em solid lighten($color-3, 30%);
|
||||||
|
|
@ -131,6 +139,21 @@ body {
|
||||||
border: 0.1em solid lighten($color-3, 30%);
|
border: 0.1em solid lighten($color-3, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropzone-group,
|
||||||
|
.dropzone-step {
|
||||||
|
height: 10px;
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hover {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#hidden-templates {
|
#hidden-templates {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,17 @@ VALUES (
|
||||||
NULL
|
NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||||
VALUES (1, 1, 'Croissant au jambon', true);
|
VALUES (1, 1, 'Croissant au jambon', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||||
|
|
||||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||||
VALUES (2, 1, 'Gratin de thon aux olives', true);
|
VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||||
|
|
||||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||||
VALUES (3, 1, 'Saumon en croute', true);
|
VALUES (3, 1, 'Saumon en croute', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||||
|
|
||||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published])
|
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||||
VALUES (4, 2, 'Ouiche lorraine', true);
|
VALUES (4, 2, 'Ouiche lorraine', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||||
|
|
||||||
|
|
||||||
-- Groups, steps and ingredients for 'Gratin de thon'.
|
-- Groups, steps and ingredients for 'Gratin de thon'.
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ CREATE TABLE [User] (
|
||||||
[is_admin] INTEGER NOT NULL DEFAULT FALSE
|
[is_admin] INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
CREATE INDEX [validation_token_index] ON [User]([validation_token]);
|
||||||
CREATE UNIQUE INDEX [User_email_index] ON [User]([email]);
|
CREATE UNIQUE INDEX [User_email_index] ON [User]([email]);
|
||||||
|
|
||||||
CREATE TABLE [UserLoginToken] (
|
CREATE TABLE [UserLoginToken] (
|
||||||
|
|
@ -58,6 +59,7 @@ CREATE TABLE [Recipe] (
|
||||||
[difficulty] INTEGER NOT NULL DEFAULT 0,
|
[difficulty] INTEGER NOT NULL DEFAULT 0,
|
||||||
[servings] INTEGER DEFAULT 4,
|
[servings] INTEGER DEFAULT 4,
|
||||||
[is_published] INTEGER NOT NULL DEFAULT FALSE,
|
[is_published] INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
[creation_datetime] TEXT NOT NULL,
|
||||||
|
|
||||||
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
|
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::{Connection, DBError, Result};
|
use super::{Connection, DBError, Result};
|
||||||
use crate::data::model;
|
use crate::data::model;
|
||||||
|
|
||||||
|
|
@ -17,7 +20,7 @@ impl Connection {
|
||||||
SELECT [id], [title]
|
SELECT [id], [title]
|
||||||
FROM [Recipe]
|
FROM [Recipe]
|
||||||
WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2)
|
WHERE [is_published] = true AND ([lang] = $1 OR [user_id] = $2)
|
||||||
ORDER BY [title]
|
ORDER BY [title] COLLATE NOCASE
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(lang)
|
.bind(lang)
|
||||||
|
|
@ -28,7 +31,7 @@ impl Connection {
|
||||||
SELECT [id], [title]
|
SELECT [id], [title]
|
||||||
FROM [Recipe]
|
FROM [Recipe]
|
||||||
WHERE [is_published] = true AND [lang] = $1
|
WHERE [is_published] = true AND [lang] = $1
|
||||||
ORDER BY [title]
|
ORDER BY [title] COLLATE NOCASE
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(lang)
|
.bind(lang)
|
||||||
|
|
@ -83,6 +86,31 @@ WHERE [Group].[id] = $1 AND [user_id] = $2
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn can_edit_recipe_all_groups(
|
||||||
|
&self,
|
||||||
|
user_id: i64,
|
||||||
|
group_ids: &[i64],
|
||||||
|
) -> Result<bool> {
|
||||||
|
let params = (0..group_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]
|
||||||
|
WHERE [Group].[id] IN ({}) AND [user_id] = $1
|
||||||
|
"#,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||||
|
for id in group_ids {
|
||||||
|
query = query.bind(id);
|
||||||
|
}
|
||||||
|
Ok(query.fetch_one(&self.pool).await? == group_ids.len() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result<bool> {
|
pub async fn can_edit_recipe_step(&self, user_id: i64, step_id: i64) -> Result<bool> {
|
||||||
sqlx::query_scalar(
|
sqlx::query_scalar(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -173,10 +201,11 @@ WHERE [Recipe].[user_id] = $1
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let db_result = sqlx::query(
|
let db_result = sqlx::query(
|
||||||
"INSERT INTO [Recipe] ([user_id], [lang], [title]) VALUES ($1, $2, '')",
|
"INSERT INTO [Recipe] ([user_id], [lang], [title], [creation_datetime]) VALUES ($1, $2, '', $3)",
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(lang)
|
.bind(lang)
|
||||||
|
.bind(Utc::now())
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -482,6 +511,22 @@ ORDER BY [name]
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_groups_order(&self, group_ids: &[i64]) -> Result<()> {
|
||||||
|
let mut tx = self.tx().await?;
|
||||||
|
|
||||||
|
for (order, id) in group_ids.iter().enumerate() {
|
||||||
|
sqlx::query("UPDATE [Group] 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_step(&self, group_id: i64) -> Result<i64> {
|
pub async fn add_recipe_step(&self, group_id: i64) -> Result<i64> {
|
||||||
let db_result = sqlx::query("INSERT INTO [Step] ([group_id]) VALUES ($1)")
|
let db_result = sqlx::query("INSERT INTO [Step] ([group_id]) VALUES ($1)")
|
||||||
.bind(group_id)
|
.bind(group_id)
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,28 @@ WHERE [email] = $1
|
||||||
Ok(GetTokenResetPasswordResult::Ok(token))
|
Ok(GetTokenResetPasswordResult::Ok(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_reset_password_token_valid(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
validation_time: Duration,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if let Some(Some(db_datetime)) = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(
|
||||||
|
r#"
|
||||||
|
SELECT [password_reset_datetime]
|
||||||
|
FROM [User]
|
||||||
|
WHERE [password_reset_token] = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(token)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(Utc::now() - db_datetime <= validation_time)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn reset_password(
|
pub async fn reset_password(
|
||||||
&self,
|
&self,
|
||||||
new_password: &str,
|
new_password: &str,
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,10 @@ async fn main() {
|
||||||
"/recipe/set_group_comment",
|
"/recipe/set_group_comment",
|
||||||
put(services::ron::set_group_comment),
|
put(services::ron::set_group_comment),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/recipe/set_groups_order",
|
||||||
|
put(services::ron::set_group_orders),
|
||||||
|
)
|
||||||
.route("/recipe/add_step", post(services::ron::add_step))
|
.route("/recipe/add_step", post(services::ron::add_step))
|
||||||
.route("/recipe/remove_step", delete(services::ron::rm_step))
|
.route("/recipe/remove_step", delete(services::ron::rm_step))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,25 @@ async fn check_user_rights_recipe_group(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_user_rights_recipe_groups(
|
||||||
|
connection: &db::Connection,
|
||||||
|
user: &Option<model::User>,
|
||||||
|
group_ids: &[i64],
|
||||||
|
) -> Result<()> {
|
||||||
|
if user.is_none()
|
||||||
|
|| !connection
|
||||||
|
.can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Err(ErrorResponse::from(ron_error(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
NOT_AUTHORIZED_MESSAGE,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn check_user_rights_recipe_step(
|
async fn check_user_rights_recipe_step(
|
||||||
connection: &db::Connection,
|
connection: &db::Connection,
|
||||||
user: &Option<model::User>,
|
user: &Option<model::User>,
|
||||||
|
|
@ -396,6 +415,17 @@ pub async fn set_group_comment(
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn set_group_orders(
|
||||||
|
State(connection): State<db::Connection>,
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
ExtractRon(ron): ExtractRon<common::ron_api::SetGroupOrders>,
|
||||||
|
) -> Result<impl IntoResponse> {
|
||||||
|
check_user_rights_recipe_groups(&connection, &user, &ron.group_ids).await?;
|
||||||
|
connection.set_groups_order(&ron.group_ids).await?;
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn add_step(
|
pub async fn add_step(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
|
|
|
||||||
|
|
@ -463,19 +463,34 @@ pub async fn ask_reset_password_post(
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn reset_password_get(
|
pub async fn reset_password_get(
|
||||||
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
Extension(tr): Extension<translation::Tr>,
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if let Some(reset_token) = query.get("reset_token") {
|
if let Some(reset_token) = query.get("reset_token") {
|
||||||
Ok(ResetPasswordTemplate {
|
// Check if the token is valid.
|
||||||
user,
|
if connection
|
||||||
tr,
|
.is_reset_password_token_valid(
|
||||||
reset_token,
|
reset_token,
|
||||||
message: "",
|
Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION),
|
||||||
message_password: "",
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(ResetPasswordTemplate {
|
||||||
|
user,
|
||||||
|
tr,
|
||||||
|
reset_token,
|
||||||
|
message: "",
|
||||||
|
message_password: "",
|
||||||
|
}
|
||||||
|
.into_response())
|
||||||
|
} else {
|
||||||
|
Ok(
|
||||||
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
||||||
|
.into_response(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.into_response())
|
|
||||||
} else {
|
} else {
|
||||||
Ok(
|
Ok(
|
||||||
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,13 @@
|
||||||
<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">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<div class="drag-handle"></div>
|
<span class="drag-handle"></span>
|
||||||
|
|
||||||
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
|
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
|
||||||
<input class="input-group-name" type="text" />
|
<input class="input-group-name" type="text" />
|
||||||
|
|
@ -96,13 +96,15 @@
|
||||||
|
|
||||||
<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>
|
<div class="steps">
|
||||||
|
<div class="dropzone-step"></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) }}" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="step">
|
<div class="step">
|
||||||
<div class="drag-handle"></div>
|
<span class="drag-handle"></span>
|
||||||
|
|
||||||
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
|
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
|
||||||
<textarea class="text-area-step-action"></textarea>
|
<textarea class="text-area-step-action"></textarea>
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,11 @@ pub struct SetGroupComment {
|
||||||
pub comment: String,
|
pub comment: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SetGroupOrders {
|
||||||
|
pub group_ids: Vec<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct AddRecipeStep {
|
pub struct AddRecipeStep {
|
||||||
pub group_id: i64,
|
pub group_id: i64,
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,22 @@ wasm-bindgen-futures = "0.4"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
"console",
|
"console",
|
||||||
"Document",
|
"Document",
|
||||||
"Element",
|
|
||||||
"HtmlElement",
|
|
||||||
"Node",
|
"Node",
|
||||||
|
"NodeList",
|
||||||
"Window",
|
"Window",
|
||||||
"Location",
|
"Location",
|
||||||
"EventTarget",
|
"EventTarget",
|
||||||
|
"DragEvent",
|
||||||
|
"DataTransfer",
|
||||||
|
"KeyboardEvent",
|
||||||
|
"Element",
|
||||||
|
"HtmlElement",
|
||||||
|
"HtmlDivElement",
|
||||||
"HtmlLabelElement",
|
"HtmlLabelElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
"HtmlTextAreaElement",
|
"HtmlTextAreaElement",
|
||||||
"HtmlSelectElement",
|
"HtmlSelectElement",
|
||||||
"HtmlDialogElement",
|
"HtmlDialogElement",
|
||||||
"KeyboardEvent",
|
|
||||||
] }
|
] }
|
||||||
|
|
||||||
gloo = "0.11"
|
gloo = "0.11"
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
use gloo::{
|
use gloo::{
|
||||||
events::EventListener,
|
console::log,
|
||||||
|
events::{EventListener, EventListenerOptions},
|
||||||
net::http::Request,
|
net::http::Request,
|
||||||
utils::{document, window},
|
utils::{document, window},
|
||||||
};
|
};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use web_sys::{Element, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, KeyboardEvent};
|
use web_sys::{
|
||||||
|
DragEvent, Element, HtmlDivElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
||||||
|
KeyboardEvent,
|
||||||
|
};
|
||||||
|
|
||||||
use common::ron_api;
|
use common::ron_api;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modal_dialog, request,
|
modal_dialog, request,
|
||||||
toast::{self, Level},
|
toast::{self, Level},
|
||||||
utils::{by_id, selector, selector_and_clone, SelectorExt},
|
utils::{by_id, selector, selector_all, selector_and_clone, SelectorExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn reload_recipes_list(current_recipe_id: i64) {
|
async fn reload_recipes_list(current_recipe_id: i64) {
|
||||||
|
|
@ -276,6 +280,92 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
})
|
})
|
||||||
.forget();
|
.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])
|
fn create_tag_elements<T>(recipe_id: i64, tags: &[T])
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: AsRef<str>,
|
||||||
|
|
@ -339,6 +429,77 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
let groups_container: Element = by_id("groups-container");
|
let groups_container: Element = by_id("groups-container");
|
||||||
groups_container.append_child(&group_element).unwrap();
|
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.
|
// Group name.
|
||||||
let name = group_element.selector::<HtmlInputElement>(".input-group-name");
|
let name = group_element.selector::<HtmlInputElement>(".input-group-name");
|
||||||
name.set_value(&group.name);
|
name.set_value(&group.name);
|
||||||
|
|
@ -388,6 +549,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
let body = ron_api::RemoveRecipeGroup { group_id };
|
let body = ron_api::RemoveRecipeGroup { group_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
let _ = request::delete::<(), _>("recipe/remove_group", body).await;
|
||||||
by_id::<Element>(&format!("group-{}", group_id)).remove();
|
by_id::<Element>(&format!("group-{}", group_id)).remove();
|
||||||
|
by_id::<Element>(&format!("dropzone-group-{}", group_id)).remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
@ -423,6 +585,69 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
group_element.append_child(&step_element).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.
|
// Step action.
|
||||||
let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action");
|
let action: HtmlTextAreaElement = step_element.selector(".text-area-step-action");
|
||||||
action.set_value(&step.action);
|
action.set_value(&step.action);
|
||||||
|
|
@ -455,6 +680,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
|
||||||
let body = ron_api::RemoveRecipeStep { step_id };
|
let body = ron_api::RemoveRecipeStep { step_id };
|
||||||
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
let _ = request::delete::<(), _>("recipe/remove_step", body).await;
|
||||||
by_id::<Element>(&format!("step-{}", step_id)).remove();
|
by_id::<Element>(&format!("step-{}", step_id)).remove();
|
||||||
|
by_id::<Element>(&format!("dropzone-step-{}", step_id)).remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use gloo::utils::document;
|
use gloo::{console::log, utils::document};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::Element;
|
use web_sys::Element;
|
||||||
|
|
||||||
|
|
@ -6,6 +6,10 @@ pub trait SelectorExt {
|
||||||
fn selector<T>(&self, selectors: &str) -> T
|
fn selector<T>(&self, selectors: &str) -> T
|
||||||
where
|
where
|
||||||
T: JsCast;
|
T: JsCast;
|
||||||
|
|
||||||
|
fn selector_all<T>(&self, selectors: &str) -> Vec<T>
|
||||||
|
where
|
||||||
|
T: JsCast;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectorExt for Element {
|
impl SelectorExt for Element {
|
||||||
|
|
@ -19,6 +23,18 @@ impl SelectorExt for Element {
|
||||||
.dyn_into::<T>()
|
.dyn_into::<T>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn selector_all<T>(&self, selectors: &str) -> Vec<T>
|
||||||
|
where
|
||||||
|
T: JsCast,
|
||||||
|
{
|
||||||
|
self.query_selector_all(selectors)
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.unwrap().dyn_into::<T>().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selector<T>(selectors: &str) -> T
|
pub fn selector<T>(selectors: &str) -> T
|
||||||
|
|
@ -33,6 +49,19 @@ where
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selector_all<T>(selectors: &str) -> Vec<T>
|
||||||
|
where
|
||||||
|
T: JsCast,
|
||||||
|
{
|
||||||
|
document()
|
||||||
|
.query_selector_all(selectors)
|
||||||
|
.unwrap()
|
||||||
|
.values()
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.unwrap().dyn_into::<T>().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selector_and_clone<T>(selectors: &str) -> T
|
pub fn selector_and_clone<T>(selectors: &str) -> T
|
||||||
where
|
where
|
||||||
T: JsCast,
|
T: JsCast,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue