From 9586611f3ef0730916d0dfd4ce814ced110f36e7 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Tue, 3 Oct 2023 11:44:43 +0530 Subject: [PATCH] Reset preferences to default btn (#385) * fix(frontend): do not allow reorder if pref change disabled * fix(frontend): make text smaller * perf(backend): select only needed attributes * perf(backend): select only partial model * fix(backend): introduce function to get entire user * feat(backend): allow setting preferences to default * feat(frontend): allow resetting preferences * fix(frontend): reload page when preferences are reloaded * build(backend): bump version * fix(frontend): wait for response before reload --- Cargo.lock | 2 +- apps/backend/Cargo.toml | 2 +- apps/backend/src/entities/user.rs | 28 +- apps/backend/src/fitness/resolver.rs | 5 +- apps/backend/src/importer/mod.rs | 11 +- apps/backend/src/miscellaneous/resolver.rs | 516 +++++++++++------- apps/backend/src/utils.rs | 15 +- .../src/pages/settings/preferences.tsx | 58 +- libs/generated/src/graphql/backend/graphql.ts | 4 + 9 files changed, 410 insertions(+), 231 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb14634403..d7f4fab4cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4547,7 +4547,7 @@ checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" [[package]] name = "ryot" -version = "2.20.0" +version = "2.20.1" dependencies = [ "anyhow", "apalis", diff --git a/apps/backend/Cargo.toml b/apps/backend/Cargo.toml index 4219e32d9d..ae60dfbdca 100644 --- a/apps/backend/Cargo.toml +++ b/apps/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ryot" -version = "2.20.0" +version = "2.20.1" edition = "2021" repository = "https://github.com/IgnisDa/ryot" license = "GPL-V3" diff --git a/apps/backend/src/entities/user.rs b/apps/backend/src/entities/user.rs index 054ce57820..d7bd0e1f09 100644 --- a/apps/backend/src/entities/user.rs +++ b/apps/backend/src/entities/user.rs @@ -6,7 +6,7 @@ use argon2::{ }; use async_graphql::SimpleObject; use async_trait::async_trait; -use sea_orm::{entity::prelude::*, ActiveValue}; +use sea_orm::{entity::prelude::*, ActiveValue, FromQueryResult}; use serde::{Deserialize, Serialize}; use crate::{ @@ -19,6 +19,32 @@ fn get_hasher() -> Argon2<'static> { Argon2::default() } +#[derive( + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromQueryResult, DerivePartialModel, +)] +#[sea_orm(entity = "Entity")] +pub struct UserWithOnlyPreferences { + pub preferences: UserPreferences, +} + +#[derive( + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromQueryResult, DerivePartialModel, +)] +#[sea_orm(entity = "Entity")] +pub struct UserWithOnlyIntegrationsAndNotifications { + pub yank_integrations: Option, + pub sink_integrations: UserSinkIntegrations, + pub notifications: UserNotifications, +} + +#[derive( + Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromQueryResult, DerivePartialModel, +)] +#[sea_orm(entity = "Entity")] +pub struct UserWithOnlySummary { + pub summary: Option, +} + #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject)] #[graphql(name = "User")] #[sea_orm(table_name = "user")] diff --git a/apps/backend/src/fitness/resolver.rs b/apps/backend/src/fitness/resolver.rs index fd605b8533..50f77d69f2 100644 --- a/apps/backend/src/fitness/resolver.rs +++ b/apps/backend/src/fitness/resolver.rs @@ -19,6 +19,7 @@ use crate::{ entities::{ exercise, prelude::{Exercise, UserMeasurement, UserToExercise, Workout}, + user::UserWithOnlyPreferences, user_measurement, user_to_exercise, workout, }, file_storage::FileStorageService, @@ -34,7 +35,7 @@ use crate::{ SearchDetails, SearchInput, SearchResults, StoredUrl, }, traits::AuthProvider, - utils::{get_case_insensitive_like_query, user_by_id}, + utils::{get_case_insensitive_like_query, partial_user_by_id}, }; use super::logic::UserWorkoutInput; @@ -505,7 +506,7 @@ impl ExerciseService { #[instrument(skip(self))] async fn create_user_workout(&self, user_id: i32, input: UserWorkoutInput) -> Result { - let user = user_by_id(&self.db, user_id).await?; + let user = partial_user_by_id::(&self.db, user_id).await?; let sf = Sonyflake::new().unwrap(); let id = sf.next_id().unwrap().to_string(); tracing::trace!("Creating new workout with id: {}", id); diff --git a/apps/backend/src/importer/mod.rs b/apps/backend/src/importer/mod.rs index fbbc5bec25..9f7acf145c 100644 --- a/apps/backend/src/importer/mod.rs +++ b/apps/backend/src/importer/mod.rs @@ -14,7 +14,7 @@ use tracing::instrument; use crate::{ background::ApplicationJob, - entities::{import_report, prelude::ImportReport}, + entities::{import_report, prelude::ImportReport, user::UserWithOnlyPreferences}, migrator::{ImportSource, MetadataLot}, miscellaneous::resolver::MiscellaneousService, models::media::{ @@ -23,7 +23,7 @@ use crate::{ }, traits::AuthProvider, users::UserReviewScale, - utils::user_by_id, + utils::partial_user_by_id, }; mod goodreads; @@ -260,9 +260,10 @@ impl ImporterService { .await? } }; - let preferences = user_by_id(&self.media_service.db, user_id) - .await? - .preferences; + let preferences = + partial_user_by_id::(&self.media_service.db, user_id) + .await? + .preferences; import.media = import .media .into_iter() diff --git a/apps/backend/src/miscellaneous/resolver.rs b/apps/backend/src/miscellaneous/resolver.rs index 1916664adb..e198e2e922 100644 --- a/apps/backend/src/miscellaneous/resolver.rs +++ b/apps/backend/src/miscellaneous/resolver.rs @@ -60,7 +60,12 @@ use crate::{ PartialMetadata as PartialMetadataModel, PartialMetadataToMetadataGroup, Person, Review, Seen, User, UserMeasurement, UserToMetadata, Workout, }, - review, seen, user, user_measurement, user_to_metadata, workout, + review, seen, + user::{ + self, UserWithOnlyIntegrationsAndNotifications, UserWithOnlyPreferences, + UserWithOnlySummary, + }, + user_measurement, user_to_metadata, workout, }, file_storage::FileStorageService, integrations::{IntegrationMedia, IntegrationService}, @@ -117,7 +122,8 @@ use crate::{ utils::{ associate_user_with_metadata, convert_naive_to_utc, get_case_insensitive_like_query, get_first_and_last_day_of_month, get_stored_asset, get_user_and_metadata_association, - user_by_id, user_id_from_token, AUTHOR, COOKIE_NAME, USER_AGENT_STR, VERSION, + partial_user_by_id, user_by_id, user_id_from_token, AUTHOR, COOKIE_NAME, USER_AGENT_STR, + VERSION, }, }; @@ -297,6 +303,8 @@ struct UpdateUserInput { #[derive(Debug, InputObject)] struct UpdateUserPreferenceInput { + /// Dot delimited path to the property that needs to be changed. Setting it\ + /// to empty resets the preferences to default. property: String, value: String, } @@ -1941,7 +1949,9 @@ impl MiscellaneousService { user_id: i32, input: MediaListInput, ) -> Result> { - let preferences = user_by_id(&self.db, user_id).await?.preferences; + let preferences = partial_user_by_id::(&self.db, user_id) + .await? + .preferences; let meta = UserToMetadata::find() .filter(user_to_metadata::Column::UserId.eq(user_id)) .apply_if( @@ -3132,7 +3142,9 @@ impl MiscellaneousService { } async fn user_preferences(&self, user_id: i32) -> Result { - let mut preferences = user_by_id(&self.db, user_id).await?.preferences; + let mut preferences = partial_user_by_id::(&self.db, user_id) + .await? + .preferences; preferences.features_enabled.media.anime = self.config.anime_and_manga.is_enabled() && preferences.features_enabled.media.anime; preferences.features_enabled.media.audio_book = @@ -3181,7 +3193,9 @@ impl MiscellaneousService { items: vec![], }); } - let preferences = user_by_id(&self.db, user_id).await?.preferences; + let preferences = partial_user_by_id::(&self.db, user_id) + .await? + .preferences; let provider = self.get_media_provider(lot, source).await?; let results = provider .search(&q, input.page, preferences.general.display_nsfw) @@ -3481,7 +3495,9 @@ impl MiscellaneousService { } async fn review_by_id(&self, review_id: i32, user_id: i32) -> Result { - let preferences = user_by_id(&self.db, user_id).await?.preferences; + let preferences = partial_user_by_id::(&self.db, user_id) + .await? + .preferences; let review = Review::find_by_id(review_id).one(&self.db).await?; match review { Some(r) => { @@ -3730,7 +3746,9 @@ impl MiscellaneousService { return Err(Error::new("At-least one of rating or review is required.")); } - let preferences = user_by_id(&self.db, user_id).await?.preferences; + let preferences = partial_user_by_id::(&self.db, user_id) + .await? + .preferences; let mut review_obj = review::ActiveModel { id: review_id, rating: ActiveValue::Set(input.rating.map( @@ -3976,7 +3994,7 @@ impl MiscellaneousService { } async fn latest_user_summary(&self, user_id: i32) -> Result { - let ls = user_by_id(&self.db, user_id).await?; + let ls = partial_user_by_id::(&self.db, user_id).await?; Ok(ls.summary.unwrap_or_default()) } @@ -4428,225 +4446,293 @@ impl MiscellaneousService { let err = || Error::new("Incorrect property value encountered"); let user_model = user_by_id(&self.db, user_id).await?; let mut preferences = user_model.preferences.clone(); - let (left, right) = input.property.split_once('.').ok_or_else(err)?; - let value_bool = input.value.parse::(); - let value_usize = input.value.parse::(); - match left { - "fitness" => { - let (left, right) = right.split_once('.').ok_or_else(err)?; + match input.property.is_empty() { + true => { + preferences = UserPreferences::default(); + } + false => { + let (left, right) = input.property.split_once('.').ok_or_else(err)?; + let value_bool = input.value.parse::(); + let value_usize = input.value.parse::(); match left { - "measurements" => { + "fitness" => { let (left, right) = right.split_once('.').ok_or_else(err)?; match left { - "custom" => { - let value_vector = serde_json::from_str(&input.value).unwrap(); - preferences.fitness.measurements.custom = value_vector; - } - "inbuilt" => match right { - "weight" => { - preferences.fitness.measurements.inbuilt.weight = - value_bool.unwrap(); - } - "body_mass_index" => { - preferences.fitness.measurements.inbuilt.body_mass_index = - value_bool.unwrap(); - } - "total_body_water" => { - preferences.fitness.measurements.inbuilt.total_body_water = - value_bool.unwrap(); - } - "muscle" => { - preferences.fitness.measurements.inbuilt.muscle = - value_bool.unwrap(); - } - "lean_body_mass" => { - preferences.fitness.measurements.inbuilt.lean_body_mass = - value_bool.unwrap(); - } - "body_fat" => { - preferences.fitness.measurements.inbuilt.body_fat = - value_bool.unwrap(); - } - "bone_mass" => { - preferences.fitness.measurements.inbuilt.bone_mass = - value_bool.unwrap(); - } - "visceral_fat" => { - preferences.fitness.measurements.inbuilt.visceral_fat = - value_bool.unwrap(); - } - "waist_circumference" => { - preferences.fitness.measurements.inbuilt.waist_circumference = - value_bool.unwrap(); - } - "waist_to_height_ratio" => { - preferences - .fitness - .measurements - .inbuilt - .waist_to_height_ratio = value_bool.unwrap(); - } - "hip_circumference" => { - preferences.fitness.measurements.inbuilt.hip_circumference = - value_bool.unwrap(); - } - "waist_to_hip_ratio" => { - preferences.fitness.measurements.inbuilt.waist_to_hip_ratio = - value_bool.unwrap(); - } - "chest_circumference" => { - preferences.fitness.measurements.inbuilt.chest_circumference = - value_bool.unwrap(); - } - "thigh_circumference" => { - preferences.fitness.measurements.inbuilt.thigh_circumference = - value_bool.unwrap(); - } - "biceps_circumference" => { - preferences - .fitness - .measurements - .inbuilt - .biceps_circumference = value_bool.unwrap(); - } - "neck_circumference" => { - preferences.fitness.measurements.inbuilt.neck_circumference = - value_bool.unwrap(); - } - "body_fat_caliper" => { - preferences.fitness.measurements.inbuilt.body_fat_caliper = - value_bool.unwrap(); - } - "chest_skinfold" => { - preferences.fitness.measurements.inbuilt.chest_skinfold = - value_bool.unwrap(); - } - "abdominal_skinfold" => { - preferences.fitness.measurements.inbuilt.abdominal_skinfold = - value_bool.unwrap(); + "measurements" => { + let (left, right) = right.split_once('.').ok_or_else(err)?; + match left { + "custom" => { + let value_vector = + serde_json::from_str(&input.value).unwrap(); + preferences.fitness.measurements.custom = value_vector; + } + "inbuilt" => match right { + "weight" => { + preferences.fitness.measurements.inbuilt.weight = + value_bool.unwrap(); + } + "body_mass_index" => { + preferences + .fitness + .measurements + .inbuilt + .body_mass_index = value_bool.unwrap(); + } + "total_body_water" => { + preferences + .fitness + .measurements + .inbuilt + .total_body_water = value_bool.unwrap(); + } + "muscle" => { + preferences.fitness.measurements.inbuilt.muscle = + value_bool.unwrap(); + } + "lean_body_mass" => { + preferences + .fitness + .measurements + .inbuilt + .lean_body_mass = value_bool.unwrap(); + } + "body_fat" => { + preferences.fitness.measurements.inbuilt.body_fat = + value_bool.unwrap(); + } + "bone_mass" => { + preferences.fitness.measurements.inbuilt.bone_mass = + value_bool.unwrap(); + } + "visceral_fat" => { + preferences.fitness.measurements.inbuilt.visceral_fat = + value_bool.unwrap(); + } + "waist_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .waist_circumference = value_bool.unwrap(); + } + "waist_to_height_ratio" => { + preferences + .fitness + .measurements + .inbuilt + .waist_to_height_ratio = value_bool.unwrap(); + } + "hip_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .hip_circumference = value_bool.unwrap(); + } + "waist_to_hip_ratio" => { + preferences + .fitness + .measurements + .inbuilt + .waist_to_hip_ratio = value_bool.unwrap(); + } + "chest_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .chest_circumference = value_bool.unwrap(); + } + "thigh_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .thigh_circumference = value_bool.unwrap(); + } + "biceps_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .biceps_circumference = value_bool.unwrap(); + } + "neck_circumference" => { + preferences + .fitness + .measurements + .inbuilt + .neck_circumference = value_bool.unwrap(); + } + "body_fat_caliper" => { + preferences + .fitness + .measurements + .inbuilt + .body_fat_caliper = value_bool.unwrap(); + } + "chest_skinfold" => { + preferences + .fitness + .measurements + .inbuilt + .chest_skinfold = value_bool.unwrap(); + } + "abdominal_skinfold" => { + preferences + .fitness + .measurements + .inbuilt + .abdominal_skinfold = value_bool.unwrap(); + } + "thigh_skinfold" => { + preferences + .fitness + .measurements + .inbuilt + .thigh_skinfold = value_bool.unwrap(); + } + "basal_metabolic_rate" => { + preferences + .fitness + .measurements + .inbuilt + .basal_metabolic_rate = value_bool.unwrap(); + } + "total_daily_energy_expenditure" => { + preferences + .fitness + .measurements + .inbuilt + .total_daily_energy_expenditure = + value_bool.unwrap(); + } + "calories" => { + preferences.fitness.measurements.inbuilt.calories = + value_bool.unwrap(); + } + _ => return Err(err()), + }, + _ => return Err(err()), } - "thigh_skinfold" => { - preferences.fitness.measurements.inbuilt.thigh_skinfold = - value_bool.unwrap(); + } + "exercises" => match right { + "save_history" => { + preferences.fitness.exercises.save_history = + value_usize.unwrap() } - "basal_metabolic_rate" => { - preferences - .fitness - .measurements - .inbuilt - .basal_metabolic_rate = value_bool.unwrap(); + "unit_system" => { + preferences.fitness.exercises.unit_system = + UserUnitSystem::from_str(&input.value).unwrap(); } - "total_daily_energy_expenditure" => { - preferences - .fitness - .measurements - .inbuilt - .total_daily_energy_expenditure = value_bool.unwrap(); + _ => return Err(err()), + }, + _ => return Err(err()), + } + } + "features_enabled" => { + let (left, right) = right.split_once('.').ok_or_else(err)?; + match left { + "fitness" => match right { + "enabled" => { + preferences.features_enabled.fitness.enabled = + value_bool.unwrap() } - "calories" => { - preferences.fitness.measurements.inbuilt.calories = - value_bool.unwrap(); + "measurements" => { + preferences.features_enabled.fitness.measurements = + value_bool.unwrap() } _ => return Err(err()), }, + "media" => { + match right { + "enabled" => { + preferences.features_enabled.media.enabled = + value_bool.unwrap() + } + "audio_book" => { + preferences.features_enabled.media.audio_book = + value_bool.unwrap() + } + "book" => { + preferences.features_enabled.media.book = + value_bool.unwrap() + } + "movie" => { + preferences.features_enabled.media.movie = + value_bool.unwrap() + } + "podcast" => { + preferences.features_enabled.media.podcast = + value_bool.unwrap() + } + "show" => { + preferences.features_enabled.media.show = + value_bool.unwrap() + } + "video_game" => { + preferences.features_enabled.media.video_game = + value_bool.unwrap() + } + "visual_novel" => { + preferences.features_enabled.media.visual_novel = + value_bool.unwrap() + } + "manga" => { + preferences.features_enabled.media.manga = + value_bool.unwrap() + } + "anime" => { + preferences.features_enabled.media.anime = + value_bool.unwrap() + } + _ => return Err(err()), + }; + } _ => return Err(err()), } } - "exercises" => match right { - "save_history" => { - preferences.fitness.exercises.save_history = value_usize.unwrap() + "notifications" => match right { + "episode_released" => { + preferences.notifications.episode_released = value_bool.unwrap() + } + "episode_name_changed" => { + preferences.notifications.episode_name_changed = value_bool.unwrap() + } + "status_changed" => { + preferences.notifications.status_changed = value_bool.unwrap() + } + "release_date_changed" => { + preferences.notifications.release_date_changed = value_bool.unwrap() } - "unit_system" => { - preferences.fitness.exercises.unit_system = - UserUnitSystem::from_str(&input.value).unwrap(); + "number_of_seasons_changed" => { + preferences.notifications.number_of_seasons_changed = + value_bool.unwrap() + } + "number_of_chapters_or_episodes_changed" => { + preferences + .notifications + .number_of_chapters_or_episodes_changed = value_bool.unwrap() } _ => return Err(err()), }, - _ => return Err(err()), - } - } - "features_enabled" => { - let (left, right) = right.split_once('.').ok_or_else(err)?; - match left { - "fitness" => match right { - "enabled" => { - preferences.features_enabled.fitness.enabled = value_bool.unwrap() + "general" => match right { + "review_scale" => { + preferences.general.review_scale = + UserReviewScale::from_str(&input.value).unwrap(); + } + "display_nsfw" => { + preferences.general.display_nsfw = value_bool.unwrap(); } - "measurements" => { - preferences.features_enabled.fitness.measurements = value_bool.unwrap() + "dashboard" => { + preferences.general.dashboard = + serde_json::from_str(&input.value).unwrap(); } _ => return Err(err()), }, - "media" => { - match right { - "enabled" => { - preferences.features_enabled.media.enabled = value_bool.unwrap() - } - "audio_book" => { - preferences.features_enabled.media.audio_book = value_bool.unwrap() - } - "book" => preferences.features_enabled.media.book = value_bool.unwrap(), - "movie" => { - preferences.features_enabled.media.movie = value_bool.unwrap() - } - "podcast" => { - preferences.features_enabled.media.podcast = value_bool.unwrap() - } - "show" => preferences.features_enabled.media.show = value_bool.unwrap(), - "video_game" => { - preferences.features_enabled.media.video_game = value_bool.unwrap() - } - "visual_novel" => { - preferences.features_enabled.media.visual_novel = - value_bool.unwrap() - } - "manga" => { - preferences.features_enabled.media.manga = value_bool.unwrap() - } - "anime" => { - preferences.features_enabled.media.anime = value_bool.unwrap() - } - _ => return Err(err()), - }; - } _ => return Err(err()), - } + }; } - "notifications" => match right { - "episode_released" => { - preferences.notifications.episode_released = value_bool.unwrap() - } - "episode_name_changed" => { - preferences.notifications.episode_name_changed = value_bool.unwrap() - } - "status_changed" => preferences.notifications.status_changed = value_bool.unwrap(), - "release_date_changed" => { - preferences.notifications.release_date_changed = value_bool.unwrap() - } - "number_of_seasons_changed" => { - preferences.notifications.number_of_seasons_changed = value_bool.unwrap() - } - "number_of_chapters_or_episodes_changed" => { - preferences - .notifications - .number_of_chapters_or_episodes_changed = value_bool.unwrap() - } - _ => return Err(err()), - }, - "general" => match right { - "review_scale" => { - preferences.general.review_scale = - UserReviewScale::from_str(&input.value).unwrap(); - } - "display_nsfw" => { - preferences.general.display_nsfw = value_bool.unwrap(); - } - "dashboard" => { - preferences.general.dashboard = serde_json::from_str(&input.value).unwrap(); - } - _ => return Err(err()), - }, - _ => return Err(err()), }; let mut user_model: user::ActiveModel = user_model.into(); user_model.preferences = ActiveValue::Set(preferences); @@ -4655,7 +4741,9 @@ impl MiscellaneousService { } async fn user_integrations(&self, user_id: i32) -> Result> { - let user = user_by_id(&self.db, user_id).await?; + let user = + partial_user_by_id::(&self.db, user_id) + .await?; let mut all_integrations = vec![]; let yank_integrations = if let Some(i) = user.yank_integrations { i.0 @@ -4709,7 +4797,9 @@ impl MiscellaneousService { &self, user_id: i32, ) -> Result> { - let user = user_by_id(&self.db, user_id).await?; + let user = + partial_user_by_id::(&self.db, user_id) + .await?; let mut all_notifications = vec![]; let notifications = user.notifications.0; notifications.into_iter().for_each(|n| { @@ -5015,7 +5105,11 @@ impl MiscellaneousService { } pub async fn yank_integrations_data_for_user(&self, user_id: i32) -> Result { - if let Some(integrations) = user_by_id(&self.db, user_id).await?.yank_integrations { + if let Some(integrations) = + partial_user_by_id::(&self.db, user_id) + .await? + .yank_integrations + { let mut progress_updates = vec![]; for integration in integrations.0.iter() { let response = match &integration.settings { @@ -5113,7 +5207,9 @@ impl MiscellaneousService { .ok_or(anyhow!("Incorrect hash id provided"))? .to_owned() .try_into()?; - let user = user_by_id(&self.db, user_id).await?; + let user = + partial_user_by_id::(&self.db, user_id) + .await?; let integration = user .sink_integrations .0 @@ -5301,7 +5397,9 @@ impl MiscellaneousService { user_id: i32, msg: &str, ) -> Result { - let user = user_by_id(&self.db, user_id).await?; + let user = + partial_user_by_id::(&self.db, user_id) + .await?; let mut success = true; for notification in user.notifications.0 { if notification.settings.send_message(msg).await.is_err() { diff --git a/apps/backend/src/utils.rs b/apps/backend/src/utils.rs index 0962ef3049..54dd371620 100644 --- a/apps/backend/src/utils.rs +++ b/apps/backend/src/utils.rs @@ -17,7 +17,7 @@ use http::header::AUTHORIZATION; use http_types::headers::HeaderName; use sea_orm::{ prelude::DateTimeUtc, ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, - DatabaseConnection, EntityTrait, QueryFilter, + DatabaseConnection, EntityTrait, PartialModelTrait, QueryFilter, }; use sea_query::{BinOper, Expr, Func, SimpleExpr}; use surf::{ @@ -217,6 +217,19 @@ pub async fn user_by_id(db: &DatabaseConnection, user_id: i32) -> Result(db: &DatabaseConnection, user_id: i32) -> Result +where + T: PartialModelTrait, +{ + User::find_by_id(user_id) + .into_partial_model::() + .one(db) + .await + .unwrap() + .ok_or_else(|| Error::new("No user found")) +} + pub fn get_first_and_last_day_of_month(year: i32, month: u32) -> (NaiveDate, NaiveDate) { let first_day = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let last_day = NaiveDate::from_ymd_opt(year, month + 1, 1) diff --git a/apps/frontend/src/pages/settings/preferences.tsx b/apps/frontend/src/pages/settings/preferences.tsx index 1b781d854d..e14a9ecc7d 100644 --- a/apps/frontend/src/pages/settings/preferences.tsx +++ b/apps/frontend/src/pages/settings/preferences.tsx @@ -4,6 +4,7 @@ import LoggedIn from "@/lib/layouts/LoggedIn"; import { gqlClient } from "@/lib/services/api"; import { DragDropContext, Draggable, Droppable } from "@hello-pangea/dnd"; import { + ActionIcon, Alert, Container, Divider, @@ -22,6 +23,7 @@ import { rem, } from "@mantine/core"; import { useListState, useLocalStorage } from "@mantine/hooks"; +import { notifications } from "@mantine/notifications"; import { DashboardElementLot, UpdateUserPreferenceDocument, @@ -29,10 +31,15 @@ import { UserReviewScale, } from "@ryot/generated/graphql/backend/graphql"; import { changeCase, snakeCase, startCase } from "@ryot/ts-utils"; -import { IconAlertCircle, IconGripVertical } from "@tabler/icons-react"; +import { + IconAlertCircle, + IconGripVertical, + IconRotate360, +} from "@tabler/icons-react"; import { useMutation } from "@tanstack/react-query"; import clsx from "clsx"; import Head from "next/head"; +import { useRouter } from "next/router"; import { Fragment, type ReactElement, useEffect } from "react"; import { match } from "ts-pattern"; import type { NextPageWithLayout } from "../_app"; @@ -91,6 +98,7 @@ const EditDashboardElement = (props: { display: "flex", justifyContent: "center", height: "100%", + cursor: "grab", }} > { const { userPreferences, coreDetails, updateUserPreferences } = usePageHooks(); + const router = useRouter(); const [dashboardElements, dashboardElementsHandlers] = useListState( userPreferences.data?.general.dashboard || [], ); @@ -176,7 +185,29 @@ const Page: NextPageWithLayout = () => { - Preferences + + Preferences + { + const yes = confirm( + "This will reset all your preferences to default. Are you sure you want to continue?", + ); + if (yes) { + await updateUserPreferences.mutateAsync({ + input: { + property: "", + value: "", + }, + }); + router.reload(); + } + }} + > + + + {!coreDetails.data.preferencesChangeAllowed ? ( } @@ -199,16 +230,21 @@ const Page: NextPageWithLayout = () => { Fitness - - The different sections on the dashboard - + The different sections on the dashboard. - dashboardElementsHandlers.reorder({ - from: source.index, - to: destination?.index || 0, - }) - } + onDragEnd={({ destination, source }) => { + if (coreDetails.data.preferencesChangeAllowed) + dashboardElementsHandlers.reorder({ + from: source.index, + to: destination?.index || 0, + }); + else + notifications.show({ + title: "Invalid action", + color: "red", + message: "Preferences can not be changed", + }); + }} > {(provided) => ( diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index e409d5d703..5c7196a132 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -1461,6 +1461,10 @@ export type UpdateUserInput = { }; export type UpdateUserPreferenceInput = { + /** + * Dot delimited path to the property that needs to be changed. Setting it\ + * to empty resets the preferences to default. + */ property: Scalars['String']['input']; value: Scalars['String']['input']; };