Ingredients can now be manually ordered
This commit is contained in:
parent
afd42ba1d0
commit
ca2227037f
11 changed files with 205 additions and 32 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -308,9 +308,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -138,10 +138,14 @@ body {
|
|||
|
||||
.step {
|
||||
border: 0.1em solid lighten($color-3, 30%);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.ingredient {
|
||||
border: 0.1em solid lighten($color-3, 30%);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
|
|
@ -160,7 +164,6 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#hidden-templates {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ VALUES (
|
|||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||
VALUES (1, 1, 'Croissant au jambon', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||
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], [creation_datetime], [servings], [estimated_time], [difficulty])
|
||||
VALUES (2, 1, 'Gratin de thon aux olives', true, '2025-01-07T10:41:05.697884837+00:00', 4, 40, 1);
|
||||
|
||||
INSERT INTO [Recipe] ([id], [user_id], [title], [is_published], [creation_datetime])
|
||||
VALUES (3, 1, 'Saumon en croute', true, '2025-01-07T10:41:05.697884837+00:00');
|
||||
|
|
@ -58,20 +58,20 @@ VALUES (2, 2, "Sel à l'origan", "", 1, "c-à-c");
|
|||
INSERT INTO [Step] ([id], [order], [group_id], [action])
|
||||
VALUES (3, 3, 2, "Mélanger au fouet et verser sur le thon dans le plat");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (3, 3, "Concentré de tomate", "", 4, "c-à-s");
|
||||
INSERT INTO [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (3, 0, 3, "Concentré de tomate", "", 4, "c-à-s");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (4, 3, "Poivre", "", 0.25, "c-à-c");
|
||||
INSERT INTO [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (4, 1, 3, "Poivre", "", 0.25, "c-à-c");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (5, 3, "Herbe de Provence", "", 0.5, "c-à-c");
|
||||
INSERT INTO [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (5, 2, 3, "Herbe de Provence", "", 0.5, "c-à-c");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (6, 3, "Crème à café ou demi-crème", "", 2, "dl");
|
||||
INSERT INTO [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (6, 3, 3, "Crème à café ou demi-crème", "", 2, "dl");
|
||||
|
||||
INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (7, 3, "Olives farcies coupées en deuxs", "", 50, "g");
|
||||
INSERT INTO [Ingredient] ([id], [order], [step_id], [name], [comment], [quantity_value], [quantity_unit])
|
||||
VALUES (7, 4, 3, "Olives farcies coupées en deuxs", "", 50, "g");
|
||||
|
||||
|
||||
INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment])
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ CREATE TABLE [Recipe] (
|
|||
[lang] TEXT NOT NULL DEFAULT 'en',
|
||||
[estimated_time] INTEGER, -- in [s].
|
||||
[description] TEXT NOT NULL DEFAULT '',
|
||||
-- 0: Unknown, 1: Easy, 2: Medium, 4: Hard.
|
||||
[difficulty] INTEGER NOT NULL DEFAULT 0,
|
||||
[servings] INTEGER DEFAULT 4,
|
||||
[is_published] INTEGER NOT NULL DEFAULT FALSE,
|
||||
|
|
@ -64,6 +65,28 @@ CREATE TABLE [Recipe] (
|
|||
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TRIGGER [Recipe_trigger_update_difficulty]
|
||||
BEFORE UPDATE OF [difficulty]
|
||||
ON [Recipe]
|
||||
BEGIN
|
||||
SELECT
|
||||
CASE
|
||||
WHEN NEW.[difficulty] < 0 OR NEW.[difficulty] > 3 THEN
|
||||
RAISE (ABORT, 'Invalid [difficulty] value')
|
||||
END;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER [Recipe_trigger_insert_difficulty]
|
||||
BEFORE INSERT
|
||||
ON [Recipe]
|
||||
BEGIN
|
||||
SELECT
|
||||
CASE
|
||||
WHEN NEW.[difficulty] < 0 OR NEW.[difficulty] > 3 THEN
|
||||
RAISE (ABORT, 'Invalid [difficulty] value')
|
||||
END;
|
||||
END;
|
||||
|
||||
CREATE TABLE [Image] (
|
||||
[Id] INTEGER PRIMARY KEY,
|
||||
[recipe_id] INTEGER NOT NULL,
|
||||
|
|
@ -124,6 +147,7 @@ CREATE INDEX [Step_order_index] ON [Group]([order]);
|
|||
CREATE TABLE [Ingredient] (
|
||||
[id] INTEGER PRIMARY KEY,
|
||||
|
||||
[order] INTEGER NOT NULL DEFAULT 0,
|
||||
[step_id] INTEGER NOT NULL,
|
||||
|
||||
[name] TEXT NOT NULL DEFAULT '',
|
||||
|
|
@ -134,14 +158,4 @@ CREATE TABLE [Ingredient] (
|
|||
FOREIGN KEY([step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
-- CREATE TABLE [IntermediateSubstance] (
|
||||
-- [id] INTEGER PRIMARY KEY,
|
||||
-- [name] TEXT NOT NULL DEFAULT '',
|
||||
-- [quantity_value] REAL,
|
||||
-- [quantity_unit] TEXT NOT NULL DEFAULT '',
|
||||
-- [output_group_id] INTEGER NOT NULL,
|
||||
-- [input_group_id] INTEGER NOT NULL,
|
||||
|
||||
-- FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE,
|
||||
-- FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE
|
||||
-- ) STRICT;
|
||||
CREATE INDEX [Ingredient_order_index] ON [Ingredient]([order]);
|
||||
|
|
|
|||
|
|
@ -172,6 +172,33 @@ WHERE [Ingredient].[id] = $1 AND [user_id] = $2
|
|||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn can_edit_recipe_all_ingredients(
|
||||
&self,
|
||||
user_id: i64,
|
||||
ingredients_ids: &[i64],
|
||||
) -> Result<bool> {
|
||||
let params = (0..ingredients_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]
|
||||
INNER JOIN [Ingredient] ON [Ingredient].[step_id] = [Step].[id]
|
||||
WHERE [Ingredient].[id] IN ({}) AND [user_id] = $1
|
||||
"#,
|
||||
params
|
||||
);
|
||||
|
||||
let mut query = sqlx::query_scalar::<_, u64>(&query_str).bind(user_id);
|
||||
for id in ingredients_ids {
|
||||
query = query.bind(id);
|
||||
}
|
||||
Ok(query.fetch_one(&self.pool).await? == ingredients_ids.len() as u64)
|
||||
}
|
||||
|
||||
pub async fn get_recipe(&self, id: i64, complete: bool) -> Result<Option<model::Recipe>> {
|
||||
match sqlx::query_as::<_, model::Recipe>(
|
||||
r#"
|
||||
|
|
@ -485,7 +512,7 @@ ORDER BY [order]
|
|||
SELECT [id], [name], [comment], [quantity_value], [quantity_unit]
|
||||
FROM [Ingredient]
|
||||
WHERE [step_id] = $1
|
||||
ORDER BY [name]
|
||||
ORDER BY [order]
|
||||
"#,
|
||||
)
|
||||
.bind(step.id)
|
||||
|
|
@ -622,10 +649,27 @@ ORDER BY [name]
|
|||
}
|
||||
|
||||
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 mut tx = self.tx().await?;
|
||||
|
||||
let last_order = sqlx::query_scalar(
|
||||
"SELECT [order] FROM [Ingredient] WHERE [step_id] = $1 ORDER BY [order] DESC LIMIT 1",
|
||||
)
|
||||
.bind(step_id)
|
||||
.execute(&self.pool)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(-1);
|
||||
|
||||
let db_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [Ingredient] ([step_id], [order])
|
||||
VALUES ($1, $2)
|
||||
"#,
|
||||
)
|
||||
.bind(step_id)
|
||||
.bind(last_order as i64)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Ok(db_result.last_insert_rowid())
|
||||
}
|
||||
|
||||
|
|
@ -681,6 +725,22 @@ ORDER BY [name]
|
|||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn set_ingredients_order(&self, ingredient_ids: &[i64]) -> Result<()> {
|
||||
let mut tx = self.tx().await?;
|
||||
|
||||
for (order, id) in ingredient_ids.iter().enumerate() {
|
||||
sqlx::query("UPDATE [Ingredient] SET [order] = $2 WHERE [id] = $1")
|
||||
.bind(id)
|
||||
.bind(order as i64)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -173,6 +173,10 @@ async fn main() {
|
|||
"/recipe/set_ingredient_unit",
|
||||
put(services::ron::set_ingredient_unit),
|
||||
)
|
||||
.route(
|
||||
"/recipe/set_ingredients_order",
|
||||
put(services::ron::set_ingredients_order),
|
||||
)
|
||||
.fallback(services::ron::not_found);
|
||||
|
||||
let fragments_routes = Router::new().route(
|
||||
|
|
|
|||
|
|
@ -180,6 +180,25 @@ async fn check_user_rights_recipe_ingredient(
|
|||
}
|
||||
}
|
||||
|
||||
async fn check_user_rights_recipe_ingredients(
|
||||
connection: &db::Connection,
|
||||
user: &Option<model::User>,
|
||||
step_ids: &[i64],
|
||||
) -> Result<()> {
|
||||
if user.is_none()
|
||||
|| !connection
|
||||
.can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, step_ids)
|
||||
.await?
|
||||
{
|
||||
Err(ErrorResponse::from(ron_error(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
NOT_AUTHORIZED_MESSAGE,
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn set_recipe_title(
|
||||
State(connection): State<db::Connection>,
|
||||
|
|
@ -579,6 +598,19 @@ pub async fn set_ingredient_unit(
|
|||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn set_ingredients_order(
|
||||
State(connection): State<db::Connection>,
|
||||
Extension(user): Extension<Option<model::User>>,
|
||||
ExtractRon(ron): ExtractRon<common::ron_api::SetIngredientOrders>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
check_user_rights_recipe_ingredients(&connection, &user, &ron.ingredient_ids).await?;
|
||||
connection
|
||||
.set_ingredients_order(&ron.ingredient_ids)
|
||||
.await?;
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
///// 404 /////
|
||||
#[debug_handler]
|
||||
pub async fn not_found(Extension(_user): Extension<Option<model::User>>) -> impl IntoResponse {
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@
|
|||
</div>
|
||||
|
||||
<div class="ingredient">
|
||||
<span class="drag-handle"></span>
|
||||
|
||||
<label for="input-ingredient-name">{{ tr.t(Sentence::RecipeIngredientName) }}</label>
|
||||
<input class="input-ingredient-name" type="text" />
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,48 @@
|
|||
{% else %}
|
||||
{% endmatch %}
|
||||
|
||||
<span class="difficulty">
|
||||
{% match recipe.difficulty %}
|
||||
{% when common::ron_api::Difficulty::Unknown %}
|
||||
{% when common::ron_api::Difficulty::Easy %}
|
||||
{{ tr.t(Sentence::RecipeDifficultyEasy) }}
|
||||
{% when common::ron_api::Difficulty::Medium %}
|
||||
{{ tr.t(Sentence::RecipeDifficultyMedium) }}
|
||||
{% when common::ron_api::Difficulty::Hard %}
|
||||
{{ tr.t(Sentence::RecipeDifficultyHard) }}
|
||||
{% endmatch %}
|
||||
</span>
|
||||
|
||||
{% if !recipe.description.is_empty() %}
|
||||
<div class="recipe-description" >
|
||||
{{ recipe.description.clone() }}
|
||||
{{ recipe.description }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for group in recipe.groups %}
|
||||
<div class="group">
|
||||
<h3>{{ group.name }}</h3>
|
||||
|
||||
<div class="steps">
|
||||
{% for step in group.steps %}
|
||||
<div class="ingredients">
|
||||
{% for ingredient in step.ingredients %}
|
||||
<div class="ingredient">
|
||||
{% if let Some(quantity) = ingredient.quantity_value %}
|
||||
{{ quantity +}}
|
||||
{{+ ingredient.quantity_unit }}
|
||||
{% endif +%}
|
||||
{{+ ingredient.name }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="step">
|
||||
{{ step.action }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -178,6 +178,11 @@ pub struct SetIngredientUnit {
|
|||
pub unit: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct SetIngredientOrders {
|
||||
pub ingredient_ids: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Tags {
|
||||
pub recipe_id: i64,
|
||||
|
|
|
|||
|
|
@ -574,6 +574,22 @@ fn create_ingredient_element(step_element: &Element, ingredient: &ron_api::Ingre
|
|||
ingredient_element.set_id(&format!("ingredient-{}", ingredient.id));
|
||||
step_element.append_child(&ingredient_element).unwrap();
|
||||
|
||||
set_draggable(&ingredient_element, "ingredient", |element| {
|
||||
let element = element.clone();
|
||||
spawn_local(async move {
|
||||
let ingredient_ids = element
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.selector_all::<Element>(".ingredient")
|
||||
.into_iter()
|
||||
.map(|e| e.id()[11..].parse::<i64>().unwrap())
|
||||
.collect();
|
||||
|
||||
let body = ron_api::SetIngredientOrders { ingredient_ids };
|
||||
let _ = request::put::<(), _>("recipe/set_ingredients_order", body).await;
|
||||
});
|
||||
});
|
||||
|
||||
// Ingredient name.
|
||||
let name: HtmlInputElement = ingredient_element.selector(".input-ingredient-name");
|
||||
name.set_value(&ingredient.name);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue