Skip to content

Commit

Permalink
Add archive feature, enhance search filter (#55)
Browse files Browse the repository at this point in the history
* Changed filter to have by keywords and sort only

added ui for archived hopefully i can figure it out

* Added Archive Feature

* last archive changes

* Add default value (false) to archive flag migration

* Clean up archive UI, add archived asset indication

* Whoop add a little more iconography

* Update filter dropdown with slick UI

* Remove "delete asset" route

* Fix typeerrors

* Remove unnecessary changes to python dependencies, requirements.txt

* Make keywords in asset search legally distinct

* Format new-asset-form

* Remove comment

* Clean up navbar-filter

---------

Co-authored-by: Thomas Shaw <[email protected]>
  • Loading branch information
pojojojo21 and printer83mph authored Apr 30, 2024
1 parent cb7b21d commit 6e21734
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 175 deletions.
1 change: 1 addition & 0 deletions backend/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Asset(Base):
author_pennkey: Mapped[str]
keywords: Mapped[str]
image_uri: Mapped[Optional[str]]
archived: Mapped[bool] = mapped_column(insert_default=False)

versions: Mapped[list["Version"]] = relationship(back_populates="asset")

Expand Down
34 changes: 34 additions & 0 deletions backend/migrations/versions/200e409da11c_add_archived_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Add archived assets
Revision ID: 200e409da11c
Revises: 33e6aa9a48d7
Create Date: 2024-04-29 16:12:48.187569
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "200e409da11c"
down_revision: Union[str, None] = "33e6aa9a48d7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"assets",
sa.Column("archived", sa.Boolean(), nullable=False, server_default=sa.false()),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("assets", "archived")
# ### end Alembic commands ###
30 changes: 11 additions & 19 deletions backend/routers/api_v1/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,26 @@
summary="Get a list of assets",
description="""Used for fetching a (paginated) list of assets stored in the database.
Allows searching by arbitrary strings, sorting by date or name, adding keyword filters, and adding offset for pagination.""",
Allows searching by arbitrary strings, sorting by date or name, adding keyword filters, and adding offset for pagination.
The 'search' field allows fuzzy searching of most fields, whereas the 'keyword' field allows for a narrower search with multiple keywords.""",
)
def get_assets(
db: Annotated[Session, Depends(get_db)],
search: str | None = None,
keywords: str | None = None,
sort: Literal["date_asc", "name_asc", "date_dsc", "name_dsc"] = "date_dsc",
sort: str | None = "name_asc",
offset: int = 0,
archived: bool | None = False,
) -> Sequence[Asset]:
# TODO: add fuzzy search somehow
return read_assets(
db, search=(search if search != "" else None), offset=offset, sort=sort
db,
search=(search if search != "" else None),
keywords=(keywords if keywords != "" else None),
offset=offset,
sort=sort,
archived=archived,
)


Expand Down Expand Up @@ -111,22 +119,6 @@ async def put_asset(
return result


@router.delete(
"/{uuid}",
summary="Delete asset metadata ONLY FOR DEV PURPOSES",
description="Based on `uuid`, deletes a specific asset.",
)
async def delete_asset(
db: Annotated[Session, Depends(get_db)],
token: Annotated[str, Depends(oauth2_scheme)],
uuid: str,
):
result = remove_asset(db, uuid)
if result is False:
raise HTTPException(status_code=404, detail="Asset not found")
return


@router.get("/{uuid}/versions", summary="Get a list of versions for a given asset")
def get_asset_versions(
db: Annotated[Session, Depends(get_db)],
Expand Down
1 change: 1 addition & 0 deletions backend/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
class AssetBase(BaseModel):
keywords: str
image_uri: Optional[str]
archived: bool = False


class AssetCreate(AssetBase):
Expand Down
44 changes: 28 additions & 16 deletions backend/util/crud/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

from util.files import temp_s3_download
from util.s3 import assets_bucket
from sqlalchemy import or_, func
from sqlalchemy.sql.expression import case
from sqlalchemy import or_, and_

# https://fastapi.tiangolo.com/tutorial/sql-databases/#crud-utils

Expand All @@ -31,8 +30,10 @@ def read_asset(db: Session, asset_id: str):
def read_assets(
db: Session,
search: str | None = None,
keywords: str | None = None,
offset=0,
sort: Literal["date_asc", "name_asc", "date_dsc", "name_dsc"] = "date_dsc",
sort: str | None = "name_asc",
archived: bool | None = False,
):
# TODO: figure out the join nonsense
# 1. distance between str and asset name should be small
Expand All @@ -44,26 +45,35 @@ def read_assets(
# If search contains commas, split by commas, otherwise split by spaces
if "," in search:
reader = csv.reader([search], skipinitialspace=True)
keywords = next(reader)
keywordList = next(reader)
else:
keywords = search.split()
# check if asset name contains search
asset_name_conditions = []
asset_name_conditions.append(
or_(
*[Asset.asset_name.ilike(f"%{search}%")],
*[Asset.asset_name.ilike(f"%{kw}%") for kw in keywords],
)
)
keywordList = search.split()

# check if keywords or author contain search words
query = query.filter(
or_(
*asset_name_conditions,
*[Asset.keywords.ilike(f"%{kw}%") for kw in keywords],
*[Asset.author_pennkey.ilike(f"%{search}%")],
Asset.asset_name.ilike(f"%{search}%"),
*[Asset.asset_name.ilike(f"%{kw}%") for kw in keywordList],
*[Asset.keywords.ilike(f"%{kw}%") for kw in keywordList],
Asset.author_pennkey.ilike(f"%{search}%"),
)
)

if keywords is not None:
# split by commas
reader = csv.reader([keywords], skipinitialspace=True)
keywordList = next(reader)

# check if keywords
query = query.filter(
and_(*[Asset.keywords.ilike(f"%{kw}%") for kw in keywordList])
)

if archived is not None:
if archived is False:
query = query.filter(Asset.archived == False)
# query = query.filter(Asset.archived == True)

# sort by date or name
# if sort == "date_asc":
# query = query.order_by(Version.date.asc())
Expand Down Expand Up @@ -94,6 +104,7 @@ def create_asset(db: Session, asset: AssetCreate, author_pennkey: str):
author_pennkey=author_pennkey,
keywords=asset.keywords,
image_uri=asset.image_uri,
archived=asset.archived,
)
db.add(db_asset)
db.commit()
Expand All @@ -111,6 +122,7 @@ def update_asset(db: Session, asset_id: str, asset: AssetUpdate):
return None
db_asset.keywords = asset.keywords
db_asset.image_uri = asset.image_uri
db_asset.archived = asset.archived
db.commit()
db.refresh(db_asset)
return db_asset
Expand Down
1 change: 1 addition & 0 deletions backend/util/sqladmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class AssetAdmin(ModelView, model=Asset):
Asset.asset_name,
Asset.author_pennkey,
Asset.keywords,
Asset.archived,
Asset.versions,
]

Expand Down
22 changes: 22 additions & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"swr": "^2.2.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"usehooks-ts": "^3.1.0",
"vite": "^5.0.12",
"zod": "^3.22.5",
"zustand": "^4.5.2"
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/renderer/src/components/asset-entry.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { MdCheckCircle, MdDownload, MdDownloading, MdLogin } from 'react-icons/md';
import { MdArchive, MdCheckCircle, MdDownload, MdDownloading, MdLogin } from 'react-icons/md';

import { useAssetSelectStore } from '@renderer/hooks/use-asset-select';
import useAuth from '@renderer/hooks/use-auth';
Expand All @@ -10,7 +10,7 @@ import funnygif from '../assets/funny.gif';
import { useNavigate } from 'react-router-dom';

export default function AssetEntry({
asset: { asset_name, author_pennkey, id, image_uri },
asset: { asset_name, author_pennkey, id, image_uri, archived },
isDownloaded,
}: {
asset: Asset;
Expand Down Expand Up @@ -74,9 +74,16 @@ export default function AssetEntry({
)}
</button>
</div>
<div className="px-1">
{asset_name}
<div className="text-xs text-base-content/50">{author_pennkey}</div>
<div className="flex w-full items-center">
<div className="px-1">
{asset_name}
<div className="text-xs text-base-content/50">{author_pennkey}</div>
</div>
{archived && (
<div className="ml-auto inline-flex h-8 w-8 items-center justify-center rounded-full bg-warning text-warning-content shadow">
<MdArchive />
</div>
)}
</div>
</button>
</li>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/renderer/src/components/forms/new-asset-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export default function NewAssetForm({ afterSubmit }: { afterSubmit?: SubmitHand
asset_name: data.assetName,
keywords: data.keywords.map(({ keyword }) => keyword).join(','),
image_uri,
archived: false,
},
headers: { Authorization: `Bearer ${await getAuthToken()}` },
});
Expand All @@ -86,6 +87,7 @@ export default function NewAssetForm({ afterSubmit }: { afterSubmit?: SubmitHand
toast.error(err instanceof Error ? `${err.message}.` : 'Something went wrong.');
return;
}

// Create initial version for the asset
await window.api.ipc('assets:create-initial-version', {
asset_id: responseData.id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
import { HiPlus, HiXMark } from 'react-icons/hi2';

import Label from './label';

interface ControlledInputProps {
inputName: string;
inputs: string[];
setInputs: (query: string[]) => void;
}

function ControlledInput({ inputName, inputs, setInputs }: ControlledInputProps) {
const [inputText, setInputText] = useState('');

useEffect(() => {
// Update internal state when store's keywords change
setInputText(''); // Clear input after store update (optional)
}, [inputs]);

const addInput = () => {
const formatted = inputText.trim().toLowerCase();
if (formatted.length === 0 || inputs.includes(formatted)) return;
const updatedInputs = [...inputs, formatted];
setInputs(updatedInputs);
setInputText('');
};

const removeInput = (index) => {
// setKeywords(keywords.filter((_, i) => i !== index));
const updatedInputs = inputs.filter((_, i) => i !== index);
setInputs(updatedInputs);
};

return (
<label className="block">
<Label label={`By ${inputName}`} />
<div className="relative w-full">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addInput();
}
}}
className="input input-bordered w-full"
/>
<button
type="button"
onClick={addInput}
disabled={inputText.length === 0}
className="btn btn-circle btn-ghost btn-sm absolute right-2 top-1/2 -translate-y-1/2"
>
<HiPlus />
</button>
</div>
<ul className="mt-3 flex w-full max-w-xs flex-wrap gap-1">
{inputs.map((input, index) => (
<li key={index}>
<button
className="btn btn-outline btn-sm inline-flex font-normal"
onClick={() => removeInput(index)}
>
{input} <HiXMark />
</button>
</li>
))}
</ul>
</label>
);
}

export default ControlledInput;
Loading

0 comments on commit 6e21734

Please sign in to comment.