Skip to content

Commit

Permalink
Column sort asc/desc for tag list (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
ciur authored Jan 25, 2025
1 parent 60e8a01 commit 8e81092
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 39 deletions.
5 changes: 3 additions & 2 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

### Adds

- Column sorting and filtering in Document Type list view
- Column sorting and filtering in Custom Field list view
- Column sorting and filtering in document type list view
- Column sorting and filtering in custom field list view
- Column sorting and filtering in tags list view


## [3.3.1] - 2025-01-19
Expand Down
40 changes: 38 additions & 2 deletions papermerge/core/features/tags/db/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import math
from typing import Tuple

from sqlalchemy import select, update, func
from sqlalchemy import select, update, func, or_
from sqlalchemy.exc import NoResultFound

from papermerge.core.exceptions import EntityNotFound
Expand All @@ -11,6 +11,17 @@
from papermerge.core import schema
from papermerge.core import orm

ORDER_BY_MAP = {
"name": orm.Tag.name.asc(),
"-name": orm.Tag.name.desc(),
"pinned": orm.Tag.pinned.asc(),
"-pinned": orm.Tag.pinned.desc(),
"description": orm.Tag.id.asc(),
"-description": orm.Tag.id.desc(),
"ID": orm.Tag.id.asc(),
"-ID": orm.Tag.id.desc(),
}


def get_tags_without_pagination(
db_session: Session, *, user_id: uuid.UUID
Expand All @@ -23,19 +34,44 @@ def get_tags_without_pagination(


def get_tags(
db_session: Session, *, user_id: uuid.UUID, page_size: int, page_number: int
db_session: Session,
*,
user_id: uuid.UUID,
page_size: int,
page_number: int,
filter: str | None = None,
order_by: str = "name",
) -> schema.PaginatedResponse[schema.Tag]:
stmt_total_tags = select(func.count(orm.Tag.id)).where(orm.Tag.user_id == user_id)

if filter:
stmt_total_tags = stmt_total_tags.where(
or_(
orm.Tag.name.icontains(filter),
orm.Tag.description.icontains(filter),
)
)

total_tags = db_session.execute(stmt_total_tags).scalar()
order_by_value = ORDER_BY_MAP.get(order_by, orm.Tag.name.asc())

offset = page_size * (page_number - 1)
stmt = (
select(orm.Tag)
.where(orm.Tag.user_id == user_id)
.limit(page_size)
.offset(offset)
.order_by(order_by_value)
)

if filter:
stmt = stmt.where(
or_(
orm.Tag.name.icontains(filter),
orm.Tag.description.icontains(filter),
)
)

db_tags = db_session.scalars(stmt).all()
items = [schema.Tag.model_validate(db_tag) for db_tag in db_tags]

Expand Down
6 changes: 4 additions & 2 deletions papermerge/core/features/tags/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from papermerge.core.features.tags.db import api as tags_dbapi
from papermerge.core.features.tags import schema as tags_schema
from papermerge.core.exceptions import EntityNotFound

from .types import PaginatedQueryParams

router = APIRouter(
prefix="/tags",
Expand Down Expand Up @@ -48,7 +48,7 @@ def retrieve_tags(
user: Annotated[
usr_schema.User, Security(get_current_user, scopes=[scopes.TAG_VIEW])
],
params: CommonQueryParams = Depends(),
params: PaginatedQueryParams = Depends(),
):
"""Retrieves (paginated) tags of the current user
Expand All @@ -60,6 +60,8 @@ def retrieve_tags(
user_id=user.id,
page_number=params.page_number,
page_size=params.page_size,
order_by=params.order_by,
filter=params.filter,
)

return tags
Expand Down
18 changes: 18 additions & 0 deletions papermerge/core/features/tags/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from enum import Enum

from papermerge.core.types import PaginatedQueryParams as BaseParams


class OrderBy(str, Enum):
name_asc = "name"
name_desc = "-name"
pinned_asc = "pinned"
pinned_desc = "-pinned"
description_asc = "description"
description_desc = "-description"
id_asc = "ID"
id_desc = "-ID"


class PaginatedQueryParams(BaseParams):
order_by: OrderBy | None = None
20 changes: 16 additions & 4 deletions ui2/src/features/tags/apiSlice.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {apiSlice} from "@/features/api/slice"
import type {
ColoredTag,
NewColoredTag,
ColoredTagUpdate,
NewColoredTag,
Paginated,
PaginatedArgs
} from "@/types"
Expand All @@ -17,9 +17,21 @@ export const apiSliceWithTags = apiSlice.injectEndpoints({
>({
query: ({
page_number = 1,
page_size = PAGINATION_DEFAULT_ITEMS_PER_PAGES
}: PaginatedArgs) =>
`/tags/?page_number=${page_number}&page_size=${page_size}`,
page_size = PAGINATION_DEFAULT_ITEMS_PER_PAGES,
sort_by = "name",
filter = undefined
}: PaginatedArgs) => {
let ret

if (filter) {
ret = `/tags/?page_number=${page_number}&page_size=${page_size}&order_by=${sort_by}`
ret += `&filter=${filter}`
} else {
ret = `/tags/?page_number=${page_number}&page_size=${page_size}&order_by=${sort_by}`
}

return ret
},
providesTags: (
result = {page_number: 1, page_size: 1, num_pages: 1, items: []},
_error,
Expand Down
40 changes: 31 additions & 9 deletions ui2/src/features/tags/components/ActionButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import QuickFilter from "@/components/QuickFilter"
import {selectFilterText, selectSelectedIds} from "@/features/tags/tagsSlice"
import {Group, Loader} from "@mantine/core"
import {useSelector} from "react-redux"
import {Group} from "@mantine/core"
import {selectSelectedIds} from "@/features/tags/tagsSlice"
import NewButton from "./NewButton"
import EditButton from "./EditButton"
import {DeleteTagsButton} from "./DeleteButton"
import EditButton from "./EditButton"
import NewButton from "./NewButton"

interface Args {
isFetching?: boolean
onQuickFilterChange: (value: string) => void
onQuickFilterClear: () => void
}

export default function ActionButtons() {
export default function ActionButtons({
isFetching,
onQuickFilterChange,
onQuickFilterClear
}: Args) {
const selectedIds = useSelector(selectSelectedIds)
const filterText = useSelector(selectFilterText)

return (
<Group>
<NewButton />
{selectedIds.length == 1 ? <EditButton tagId={selectedIds[0]} /> : ""}
{selectedIds.length >= 1 ? <DeleteTagsButton /> : ""}
<Group justify="space-between">
<Group>
<NewButton />
{selectedIds.length == 1 ? <EditButton tagId={selectedIds[0]} /> : ""}
{selectedIds.length >= 1 ? <DeleteTagsButton /> : ""}
{isFetching && <Loader size={"sm"} />}
</Group>
<Group>
<QuickFilter
onChange={onQuickFilterChange}
onClear={onQuickFilterClear}
filterText={filterText}
/>
</Group>
</Group>
)
}
106 changes: 90 additions & 16 deletions ui2/src/features/tags/components/List.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
import {useState} from "react"
import {Center, Stack, Table, Checkbox, Loader} from "@mantine/core"
import {useDispatch, useSelector} from "react-redux"
import Th from "@/components/TableSort/Th"
import {useGetPaginatedTagsQuery} from "@/features/tags/apiSlice"
import {
selectionAddMany,
selectSelectedIds,
clearSelection,
filterUpdated,
lastPageSizeUpdate,
selectLastPageSize,
lastPageSizeUpdate
selectReverseSortedByDescription,
selectReverseSortedByID,
selectReverseSortedByName,
selectReverseSortedByPinned,
selectSelectedIds,
selectSortedByDescription,
selectSortedByID,
selectSortedByName,
selectSortedByPinned,
selectTableSortColumns,
selectionAddMany,
sortByUpdated
} from "@/features/tags/tagsSlice"
import {useGetPaginatedTagsQuery} from "@/features/tags/apiSlice"
import {Center, Checkbox, Loader, Stack, Table} from "@mantine/core"
import {useState} from "react"
import {useDispatch, useSelector} from "react-redux"

import Pagination from "@/components/Pagination"
import TagRow from "./TagRow"
import type {TagsListColumnName} from "../types"
import ActionButtons from "./ActionButtons"
import TagRow from "./TagRow"

export default function TagsList() {
const selectedIds = useSelector(selectSelectedIds)
const dispatch = useDispatch()
const tableSortCols = useSelector(selectTableSortColumns)
const lastPageSize = useSelector(selectLastPageSize)
const sortedByName = useSelector(selectSortedByName)
const sortedByPinned = useSelector(selectSortedByPinned)
const sortedByDescription = useSelector(selectSortedByDescription)
const sortedByID = useSelector(selectSortedByID)
const reverseSortedByName = useSelector(selectReverseSortedByName)
const reverseSortedByPinned = useSelector(selectReverseSortedByPinned)
const reverseSortedByDescription = useSelector(
selectReverseSortedByDescription
)
const reverseSortedByID = useSelector(selectReverseSortedByID)

const [page, setPage] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(lastPageSize)

const {data, isLoading, isFetching} = useGetPaginatedTagsQuery({
page_number: page,
page_size: pageSize
page_size: pageSize,
sort_by: tableSortCols.sortBy,
filter: tableSortCols.filter
})

const onCheckAll = (checked: boolean) => {
Expand Down Expand Up @@ -53,10 +79,27 @@ export default function TagsList() {
}
}

const onSortBy = (columnName: TagsListColumnName) => {
dispatch(sortByUpdated(columnName))
}

const onQuickFilterChange = (value: string) => {
dispatch(filterUpdated(value))
setPage(1)
}

const onQuickFilterClear = () => {
dispatch(filterUpdated(undefined))
setPage(1)
}

if (isLoading || !data) {
return (
<Stack>
<ActionButtons />
<ActionButtons
onQuickFilterChange={onQuickFilterChange}
onQuickFilterClear={onQuickFilterClear}
/>
<Center>
<Loader type="bars" />
</Center>
Expand All @@ -67,7 +110,10 @@ export default function TagsList() {
if (data.items.length == 0) {
return (
<div>
<ActionButtons />
<ActionButtons
onQuickFilterChange={onQuickFilterChange}
onQuickFilterClear={onQuickFilterClear}
/>
<Empty />
</div>
)
Expand All @@ -77,7 +123,11 @@ export default function TagsList() {

return (
<Stack>
<ActionButtons /> {isFetching && <Loader size={"sm"} />}
<ActionButtons
isFetching={isFetching}
onQuickFilterChange={onQuickFilterChange}
onQuickFilterClear={onQuickFilterClear}
/>
<Table>
<Table.Thead>
<Table.Tr>
Expand All @@ -87,10 +137,34 @@ export default function TagsList() {
onChange={e => onCheckAll(e.currentTarget.checked)}
/>
</Table.Th>
<Table.Th>Name</Table.Th>
<Table.Th>Pinned?</Table.Th>
<Table.Th>Description</Table.Th>
<Table.Th>ID</Table.Th>
<Th
sorted={sortedByName}
reversed={reverseSortedByName}
onSort={() => onSortBy("name")}
>
Name
</Th>
<Th
sorted={sortedByPinned}
reversed={reverseSortedByPinned}
onSort={() => onSortBy("pinned")}
>
Pinned?
</Th>
<Th
sorted={sortedByDescription}
reversed={reverseSortedByDescription}
onSort={() => onSortBy("description")}
>
Description
</Th>
<Th
sorted={sortedByID}
reversed={reverseSortedByID}
onSort={() => onSortBy("ID")}
>
ID
</Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{tagRows}</Table.Tbody>
Expand Down
Loading

0 comments on commit 8e81092

Please sign in to comment.