Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Arrangeable sidebar and topbar #308

Merged
merged 7 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ build
.next
.expo

packages/browser/src/i18n/locales/*.json
packages/i18n/src/locales/*.json

CHANGELOG.md
7 changes: 4 additions & 3 deletions apps/server/src/routers/api/v1/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,10 @@ pub async fn register(
// supported on the prisma client. Until then, this ugly mess is necessary.
let _user_preferences = db
.user_preferences()
.create(vec![user_preferences::user::connect(user::id::equals(
created_user.id.clone(),
))])
.create(vec![
user_preferences::user::connect(user::id::equals(created_user.id.clone())),
user_preferences::user_id::set(Some(created_user.id.clone())),
])
.exec()
.await?;

Expand Down
95 changes: 93 additions & 2 deletions apps/server/src/routers/api/v1/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use specta::Type;
use stump_core::{
config::StumpConfig,
db::{
entity::{AgeRestriction, LoginActivity, User, UserPermission, UserPreferences},
entity::{
AgeRestriction, Arrangement, LoginActivity, NavigationItem, User,
UserPermission, UserPreferences,
},
query::pagination::{Pageable, Pagination, PaginationQuery},
},
filesystem::{
Expand Down Expand Up @@ -50,7 +53,11 @@ pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
"/users/me",
Router::new()
.route("/", put(update_current_user))
.route("/preferences", put(update_current_user_preferences)),
.route("/preferences", put(update_current_user_preferences))
.route(
"/navigation-arrangement",
get(get_navigation_arrangement).put(update_navigation_arrangement),
),
)
.nest(
"/users/:id",
Expand Down Expand Up @@ -609,6 +616,90 @@ async fn update_current_user_preferences(
Ok(Json(updated_preferences))
}

#[utoipa::path(
get,
path = "/api/v1/users/me/navigation-arrangement",
tag = "user",
responses(
(status = 200, description = "Successfully fetched user navigation arrangement"),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "User preferences not found"),
(status = 500, description = "Internal server error"),
)
)]
async fn get_navigation_arrangement(
session: Session,
State(ctx): State<AppState>,
) -> APIResult<Json<Arrangement<NavigationItem>>> {
let user = get_session_user(&session)?;
let db = &ctx.db;

let user_preferences = db
.user_preferences()
.find_unique(user_preferences::user_id::equals(user.id.clone()))
.exec()
.await?
.ok_or(APIError::NotFound(format!(
"User preferences with id {} not found",
user.id
)))?;
let user_preferences = UserPreferences::from(user_preferences);

Ok(Json(user_preferences.navigation_arrangement))
}

#[utoipa::path(
put,
path = "/api/v1/users/me/navigation-arrangement",
tag = "user",
request_body = Arrangement<NavigationItem>,
responses(
(status = 200, description = "Successfully updated user navigation arrangement", body = Arrangement<NavigationItem>),
(status = 401, description = "Unauthorized"),
(status = 403, description = "Forbidden"),
(status = 404, description = "User preferences not found"),
(status = 500, description = "Internal server error"),
)
)]
async fn update_navigation_arrangement(
session: Session,
State(ctx): State<AppState>,
Json(input): Json<Arrangement<NavigationItem>>,
) -> APIResult<Json<Arrangement<NavigationItem>>> {
let user = get_session_user(&session)?;
let db = &ctx.db;

let user_preferences = db
.user_preferences()
.find_unique(user_preferences::user_id::equals(user.id.clone()))
.exec()
.await?
.ok_or(APIError::NotFound(format!(
"User preferences with id {} not found",
user.id
)))?;
let user_preferences = UserPreferences::from(user_preferences);

let _updated_preferences = db
.user_preferences()
.update(
user_preferences::id::equals(user_preferences.id.clone()),
vec![user_preferences::navigation_arrangement::set(Some(
serde_json::to_vec(&input).map_err(|e| {
APIError::InternalServerError(format!(
"Failed to serialize navigation arrangement: {}",
e
))
})?,
))],
)
.exec()
.await?;

Ok(Json(input))
}

#[derive(Deserialize, Type, ToSchema)]
pub struct DeleteUser {
pub hard_delete: Option<bool>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "user_preferences" ADD COLUMN "home_arrangement" BLOB;
ALTER TABLE "user_preferences" ADD COLUMN "navigation_arrangement" BLOB;
3 changes: 3 additions & 0 deletions core/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,9 @@ model UserPreferences {
prefer_accent_color Boolean @default(true)
show_thumbnails_in_headers Boolean @default(false)

navigation_arrangement Bytes?
home_arrangement Bytes?

user User?
user_id String? @unique

Expand Down
157 changes: 157 additions & 0 deletions core/src/db/entity/user/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,125 @@ use utoipa::ToSchema;

use crate::prisma;

#[derive(Default, Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
pub enum NavigationMode {
#[default]
#[serde(rename = "SIDEBAR")]
SideBar,
#[serde(rename = "TOPBAR")]
TopBar,
}

// TODO: support order_by for some options with actions

#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
pub struct NaviationItemDisplayOptions {
#[serde(default = "default_true")]
pub show_create_action: bool,
#[serde(default)]
pub show_link_to_all: bool,
}

impl Default for NaviationItemDisplayOptions {
fn default() -> Self {
Self {
show_create_action: true,
show_link_to_all: false,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
#[serde(tag = "type")]
pub enum NavigationItem {
Home,
Explore,
Libraries(NaviationItemDisplayOptions),
SmartLists(NaviationItemDisplayOptions),
BookClubs(NaviationItemDisplayOptions),
}

// TODO: support order_by in some options (e.g. Library)

#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
#[serde(tag = "type")]
pub enum HomeItem {
ContinueReading,
RecentlyAddedBooks,
RecentlyAddedSeries,
Library { library_id: String },
SmartList { smart_list_id: String },
}

#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
pub struct ArrangementItem<I> {
item: I,
#[serde(default = "default_true")]
visible: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
pub struct Arrangement<I> {
locked: bool,
items: Vec<ArrangementItem<I>>,
}

impl<I> Arrangement<I> {
pub fn default_navigation() -> Arrangement<NavigationItem> {
Arrangement {
locked: true,
items: vec![
ArrangementItem {
item: NavigationItem::Home,
visible: true,
},
ArrangementItem {
item: NavigationItem::Explore,
visible: true,
},
ArrangementItem {
item: NavigationItem::Libraries(
NaviationItemDisplayOptions::default(),
),
visible: true,
},
ArrangementItem {
item: NavigationItem::SmartLists(
NaviationItemDisplayOptions::default(),
),
visible: true,
},
ArrangementItem {
item: NavigationItem::BookClubs(
NaviationItemDisplayOptions::default(),
),
visible: true,
},
],
}
}

pub fn default_home() -> Arrangement<HomeItem> {
Arrangement {
locked: true,
items: vec![
ArrangementItem {
item: HomeItem::ContinueReading,
visible: true,
},
ArrangementItem {
item: HomeItem::RecentlyAddedBooks,
visible: true,
},
ArrangementItem {
item: HomeItem::RecentlyAddedSeries,
visible: true,
},
],
}
}
}

fn default_navigation_mode() -> String {
"SIDEBAR".to_string()
}
Expand Down Expand Up @@ -48,6 +167,11 @@ pub struct UserPreferences {
pub prefer_accent_color: bool,
#[serde(default)]
pub show_thumbnails_in_headers: bool,

#[serde(default = "Arrangement::<NavigationItem>::default_navigation")]
pub navigation_arrangement: Arrangement<NavigationItem>,
#[serde(default = "Arrangement::<HomeItem>::default_home")]
pub home_arrangement: Arrangement<HomeItem>,
}

impl Default for UserPreferences {
Expand All @@ -68,6 +192,8 @@ impl Default for UserPreferences {
enable_hide_scrollbar: false,
prefer_accent_color: true,
show_thumbnails_in_headers: false,
navigation_arrangement: Arrangement::<NavigationItem>::default_navigation(),
home_arrangement: Arrangement::<HomeItem>::default_home(),
}
}
}
Expand All @@ -78,6 +204,35 @@ impl Default for UserPreferences {

impl From<prisma::user_preferences::Data> for UserPreferences {
fn from(data: prisma::user_preferences::Data) -> UserPreferences {
let navigation_arrangement = data
.navigation_arrangement
.map(|bytes| {
serde_json::from_slice(&bytes).map_or_else(
|error| {
tracing::error!(
?error,
"Failed to deserialize navigation arrangement"
);
Arrangement::<NavigationItem>::default_navigation()
},
|v| v,
)
})
.unwrap_or_else(Arrangement::<NavigationItem>::default_navigation);

let home_arrangement = data
.home_arrangement
.map(|bytes| {
serde_json::from_slice(&bytes).map_or_else(
|error| {
tracing::error!(?error, "Failed to deserialize home arrangement");
Arrangement::<HomeItem>::default_home()
},
|v| v,
)
})
.unwrap_or_else(Arrangement::<HomeItem>::default_home);

UserPreferences {
id: data.id,
locale: data.locale,
Expand All @@ -94,6 +249,8 @@ impl From<prisma::user_preferences::Data> for UserPreferences {
enable_hide_scrollbar: data.enable_hide_scrollbar,
prefer_accent_color: data.prefer_accent_color,
show_thumbnails_in_headers: data.show_thumbnails_in_headers,
navigation_arrangement,
home_arrangement,
}
}
}
12 changes: 12 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,19 @@ mod tests {
file.write_all(format!("{}\n\n", ts_export::<User>()?).as_bytes())?;
file.write_all(format!("{}\n\n", ts_export::<UserPermission>()?).as_bytes())?;
file.write_all(format!("{}\n\n", ts_export::<AgeRestriction>()?).as_bytes())?;

file.write_all(format!("{}\n\n", ts_export::<NavigationMode>()?).as_bytes())?;
file.write_all(format!("{}\n\n", ts_export::<HomeItem>()?).as_bytes())?;
file.write_all(
format!("{}\n\n", ts_export::<NaviationItemDisplayOptions>()?).as_bytes(),
)?;
file.write_all(format!("{}\n\n", ts_export::<NavigationItem>()?).as_bytes())?;
file.write_all(
format!("{}\n\n", ts_export::<ArrangementItem<()>>()?).as_bytes(),
)?;
file.write_all(format!("{}\n\n", ts_export::<Arrangement<()>>()?).as_bytes())?;
file.write_all(format!("{}\n\n", ts_export::<UserPreferences>()?).as_bytes())?;

file.write_all(format!("{}\n\n", ts_export::<LoginActivity>()?).as_bytes())?;

file.write_all(format!("{}\n\n", ts_export::<FileStatus>()?).as_bytes())?;
Expand Down
Loading
Loading