Skip to content

Commit

Permalink
Filters (#20)
Browse files Browse the repository at this point in the history
* feat(backend): add todo for feature to be implemented

* feat(backend): allow sorting in media list

Also regenerate typescript definitions.

* feat(frontend): allow sorting

* fix(backend): media release date sort by

* fix(frontend): set default sort by to `ReleaseDate`

* build(backend): bump version

* feat(backend): make sort argument optional

* fix(backend): deassociate user and metadata when unseen

* feat(backend): add a success field to media import report
  • Loading branch information
IgnisDa authored May 10, 2023
1 parent f4007ff commit 01da085
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 60 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ryot"
version = "0.0.14"
version = "0.0.15"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/entities/media_import_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct Model {
pub started_on: DateTimeUtc,
pub finished_on: Option<DateTimeUtc>,
pub details: Option<ImportResultResponse>,
pub success: Option<bool>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/importer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ impl ImporterService {
}
}

pub async fn deploy_goodreads_import(&self, user_id: i32) -> Result<String> {
todo!("Implement import from good read using the CSV export.");
}

pub async fn deploy_media_tracker_import(
&self,
user_id: i32,
Expand Down
86 changes: 67 additions & 19 deletions apps/backend/src/media/resolver.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use async_graphql::{Context, Enum, Error, InputObject, Object, Result, SimpleObject};
use chrono::{NaiveDate, Utc};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait,
ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, Order,
PaginatorTrait, QueryFilter, QueryOrder,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -111,16 +111,48 @@ pub struct DatabaseMediaDetails {
pub audio_books_specifics: Option<AudioBookSpecifics>,
}

#[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy, Default)]
pub enum MediaSortOrder {
Desc,
#[default]
Asc,
}

impl From<MediaSortOrder> for Order {
fn from(value: MediaSortOrder) -> Self {
match value {
MediaSortOrder::Desc => Self::Desc,
MediaSortOrder::Asc => Self::Asc,
}
}
}

#[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy, Default)]
pub enum MediaSortBy {
Title,
#[default]
ReleaseDate,
}

#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
pub struct MediaConsumedInput {
pub identifier: String,
pub lot: MetadataLot,
pub struct MediaSortInput {
#[graphql(default)]
pub order: MediaSortOrder,
#[graphql(default)]
pub by: MediaSortBy,
}

#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
pub struct MediaListInput {
pub page: i32,
pub lot: MetadataLot,
pub sort: Option<MediaSortInput>,
}

#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
pub struct MediaConsumedInput {
pub identifier: String,
pub lot: MetadataLot,
}

#[derive(Serialize, Deserialize, Debug, InputObject)]
Expand Down Expand Up @@ -425,30 +457,31 @@ impl MediaService {
let condition = Metadata::find()
.filter(metadata::Column::Lot.eq(input.lot))
.filter(metadata::Column::Id.is_in(distinct_meta_ids));
let (sort_by, sort_order) = match input.sort {
None => (metadata::Column::Id, Order::Asc),
Some(s) => (
match s.by {
MediaSortBy::Title => metadata::Column::Title,
MediaSortBy::ReleaseDate => metadata::Column::PublishYear,
},
Order::from(s.order),
),
};
let condition = condition.order_by(sort_by, sort_order);
let counts = condition.clone().count(&self.db).await.unwrap();
let paginator = condition.paginate(&self.db, LIMIT as u64);
let metas = paginator.fetch_page((input.page - 1) as u64).await.unwrap();
let mut items = vec![];
for m in metas {
let mut images = Metadata::find_by_id(m.id)
.find_with_related(MetadataImage)
.all(&self.db)
.await
.unwrap();
let images = images.remove(0).1;
let poster_images = images
.iter()
.filter(|f| f.lot == MetadataImageLot::Poster)
.map(|i| i.url.clone())
.collect();
let _m = MediaSearchItem {
let (poster_images, _) = self.metadata_images(&m).await?;
let m_smol = MediaSearchItem {
identifier: m.id.to_string(),
lot: m.lot,
title: m.title,
poster_images,
publish_year: m.publish_year,
};
items.push(_m);
items.push(m_smol);
}
Ok(MediaSearchResults {
total: counts as i32,
Expand Down Expand Up @@ -541,14 +574,29 @@ impl MediaService {
pub async fn delete_seen_item(&self, seen_id: i32, user_id: i32) -> Result<IdObject> {
let seen_item = Seen::find_by_id(seen_id).one(&self.db).await.unwrap();
if let Some(si) = seen_item {
let id = si.id;
let seen_id = si.id;
let metadata_id = si.metadata_id;
if si.user_id != user_id {
return Err(Error::new(
"This seen item does not belong to this user".to_owned(),
));
}
si.delete(&self.db).await.ok();
Ok(IdObject { id })
let count = Seen::find()
.filter(seen::Column::UserId.eq(user_id))
.filter(seen::Column::MetadataId.eq(metadata_id))
.count(&self.db)
.await
.unwrap();
if count == 0 {
UserToMetadata::delete_many()
.filter(user_to_metadata::Column::UserId.eq(user_id))
.filter(user_to_metadata::Column::MetadataId.eq(metadata_id))
.exec(&self.db)
.await
.ok();
}
Ok(IdObject { id: seen_id })
} else {
Err(Error::new("This seen item does not exist".to_owned()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum MediaImportReport {
FinishedOn,
Source,
Details,
Success,
}

impl MigrationName for Migration {
Expand Down Expand Up @@ -65,6 +66,7 @@ impl MigrationTrait for Migration {
)
.col(ColumnDef::new(MediaImportReport::FinishedOn).timestamp_with_time_zone())
.col(ColumnDef::new(MediaImportReport::Details).json())
.col(ColumnDef::new(MediaImportReport::Success).boolean())
.foreign_key(
ForeignKey::create()
.name("media_import_report_to_user_foreign_key")
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/misc/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ impl MiscService {
let mut model: media_import_report::ActiveModel = job.into();
model.finished_on = ActiveValue::Set(Some(Utc::now()));
model.details = ActiveValue::Set(Some(details));
model.success = ActiveValue::Set(Some(true));
let model = model.update(&self.db).await.unwrap();
Ok(model)
}
Expand Down
124 changes: 85 additions & 39 deletions apps/frontend/src/pages/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,47 @@ import {
Box,
Center,
Container,
Group,
Pagination,
Select,
Stack,
Tabs,
Text,
TextInput,
} from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import { useLocalStorage, useToggle } from "@mantine/hooks";
import {
AudioBooksSearchDocument,
BooksSearchDocument,
MediaListDocument,
MediaSortBy,
MediaSortOrder,
MetadataLot,
MoviesSearchDocument,
ShowsSearchDocument,
VideoGamesSearchDocument,
} from "@ryot/generated/graphql/backend/graphql";
import { IconListCheck, IconRefresh, IconSearch } from "@tabler/icons-react";
import {
IconListCheck,
IconRefresh,
IconSearch,
IconSortAscending,
IconSortDescending,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { lowerCase, startCase } from "lodash";
import { useRouter } from "next/router";
import { type ReactElement } from "react";
import { type ReactElement, useState } from "react";
import invariant from "tiny-invariant";
import { match } from "ts-pattern";

const LIMIT = 20;

const Page: NextPageWithLayout = () => {
const [mineSortOrder, toggleMineSortOrder] = useToggle(
Object.values(MediaSortOrder),
);
const [mineSortBy, setMineSortBy] = useState(MediaSortBy.ReleaseDate);
const [activeSearchPage, setSearchPage] = useLocalStorage({
key: "savedSearchPage",
});
Expand All @@ -52,11 +67,15 @@ const Page: NextPageWithLayout = () => {
const lot = getLot(router.query.lot);
const offset = (parseInt(activeSearchPage || "1") - 1) * LIMIT;
const listMedia = useQuery({
queryKey: ["listMedia", activeMinePage, lot],
queryKey: ["listMedia", activeMinePage, lot, mineSortBy, mineSortOrder],
queryFn: async () => {
invariant(lot, "Lot is not defined");
const { mediaList } = await gqlClient.request(MediaListDocument, {
input: { lot, page: parseInt(activeMinePage) || 1 },
input: {
lot,
page: parseInt(activeMinePage) || 1,
sort: { order: mineSortOrder, by: mineSortBy },
},
});
return mediaList;
},
Expand Down Expand Up @@ -119,14 +138,14 @@ const Page: NextPageWithLayout = () => {

return lot ? (
<Container>
<Tabs variant="outline" defaultValue="search">
<Tabs variant="outline" defaultValue="mine">
<Tabs.List mb={"xs"}>
<Tabs.Tab value="search" icon={<IconSearch size="1.5rem" />}>
<Text size={"lg"}>Search</Text>
</Tabs.Tab>
<Tabs.Tab value="mine" icon={<IconListCheck size="1.5rem" />}>
<Text size={"lg"}>My {changeCase(lot.toLowerCase())}s</Text>
</Tabs.Tab>
<Tabs.Tab value="search" icon={<IconSearch size="1.5rem" />}>
<Text size={"lg"}>Search</Text>
</Tabs.Tab>
<Box style={{ flexGrow: 1 }}>
<ActionIcon
size="lg"
Expand All @@ -144,6 +163,63 @@ const Page: NextPageWithLayout = () => {
</Box>
</Tabs.List>

<Tabs.Panel value="mine">
<Stack>
<Group>
<Select
size="xs"
data={Object.values(MediaSortBy).map((o) => ({
value: o.toString(),
label: startCase(lowerCase(o)),
}))}
defaultValue={mineSortBy.toString()}
onChange={(v) => {
const orderBy = match(v)
.with("RELEASE_DATE", () => MediaSortBy.ReleaseDate)
.with("TITLE", () => MediaSortBy.Title)
.otherwise(() => MediaSortBy.Title);
setMineSortBy(orderBy);
}}
/>
<ActionIcon onClick={() => toggleMineSortOrder()}>
{mineSortOrder === MediaSortOrder.Asc ? (
<IconSortAscending />
) : (
<IconSortDescending />
)}
</ActionIcon>
</Group>
{listMedia.data && listMedia.data.total > 0 ? (
<>
<Grid>
{listMedia.data.items.map((lm) => (
<MediaItemWithoutUpdateModal
key={lm.identifier}
item={lm}
lot={lot}
imageOnClick={async () => parseInt(lm.identifier)}
/>
))}
</Grid>
</>
) : (
<Text>You do not have any saved yet</Text>
)}
{listMedia.data && (
<Center>
<Pagination
size="sm"
value={parseInt(activeMinePage)}
onChange={(v) => setMinePage(v.toString())}
total={Math.ceil(listMedia.data.total / LIMIT)}
boundaries={1}
siblings={0}
/>
</Center>
)}
</Stack>
</Tabs.Panel>

<Tabs.Panel value="search">
<Stack>
<TextInput
Expand Down Expand Up @@ -185,36 +261,6 @@ const Page: NextPageWithLayout = () => {
)}
</Stack>
</Tabs.Panel>
<Tabs.Panel value="mine">
<Stack>
{listMedia.data && listMedia.data.total > 0 ? (
<Grid>
{listMedia.data.items.map((lm) => (
<MediaItemWithoutUpdateModal
key={lm.identifier}
item={lm}
lot={lot}
imageOnClick={async () => parseInt(lm.identifier)}
/>
))}
</Grid>
) : (
<Text>You do not have any saved yet</Text>
)}
{listMedia.data && (
<Center>
<Pagination
size="sm"
value={parseInt(activeMinePage)}
onChange={(v) => setMinePage(v.toString())}
total={Math.ceil(listMedia.data.total / LIMIT)}
boundaries={1}
siblings={0}
/>
</Center>
)}
</Stack>
</Tabs.Panel>
</Tabs>
</Container>
) : null;
Expand Down
Loading

0 comments on commit 01da085

Please sign in to comment.