Skip to content

Commit 8e81092

Browse files
authored
Column sort asc/desc for tag list (#581)
1 parent 60e8a01 commit 8e81092

File tree

9 files changed

+353
-39
lines changed

9 files changed

+353
-39
lines changed

changelog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
### Adds
1212

13-
- Column sorting and filtering in Document Type list view
14-
- Column sorting and filtering in Custom Field list view
13+
- Column sorting and filtering in document type list view
14+
- Column sorting and filtering in custom field list view
15+
- Column sorting and filtering in tags list view
1516

1617

1718
## [3.3.1] - 2025-01-19

papermerge/core/features/tags/db/api.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import math
33
from typing import Tuple
44

5-
from sqlalchemy import select, update, func
5+
from sqlalchemy import select, update, func, or_
66
from sqlalchemy.exc import NoResultFound
77

88
from papermerge.core.exceptions import EntityNotFound
@@ -11,6 +11,17 @@
1111
from papermerge.core import schema
1212
from papermerge.core import orm
1313

14+
ORDER_BY_MAP = {
15+
"name": orm.Tag.name.asc(),
16+
"-name": orm.Tag.name.desc(),
17+
"pinned": orm.Tag.pinned.asc(),
18+
"-pinned": orm.Tag.pinned.desc(),
19+
"description": orm.Tag.id.asc(),
20+
"-description": orm.Tag.id.desc(),
21+
"ID": orm.Tag.id.asc(),
22+
"-ID": orm.Tag.id.desc(),
23+
}
24+
1425

1526
def get_tags_without_pagination(
1627
db_session: Session, *, user_id: uuid.UUID
@@ -23,19 +34,44 @@ def get_tags_without_pagination(
2334

2435

2536
def get_tags(
26-
db_session: Session, *, user_id: uuid.UUID, page_size: int, page_number: int
37+
db_session: Session,
38+
*,
39+
user_id: uuid.UUID,
40+
page_size: int,
41+
page_number: int,
42+
filter: str | None = None,
43+
order_by: str = "name",
2744
) -> schema.PaginatedResponse[schema.Tag]:
2845
stmt_total_tags = select(func.count(orm.Tag.id)).where(orm.Tag.user_id == user_id)
46+
47+
if filter:
48+
stmt_total_tags = stmt_total_tags.where(
49+
or_(
50+
orm.Tag.name.icontains(filter),
51+
orm.Tag.description.icontains(filter),
52+
)
53+
)
54+
2955
total_tags = db_session.execute(stmt_total_tags).scalar()
56+
order_by_value = ORDER_BY_MAP.get(order_by, orm.Tag.name.asc())
3057

3158
offset = page_size * (page_number - 1)
3259
stmt = (
3360
select(orm.Tag)
3461
.where(orm.Tag.user_id == user_id)
3562
.limit(page_size)
3663
.offset(offset)
64+
.order_by(order_by_value)
3765
)
3866

67+
if filter:
68+
stmt = stmt.where(
69+
or_(
70+
orm.Tag.name.icontains(filter),
71+
orm.Tag.description.icontains(filter),
72+
)
73+
)
74+
3975
db_tags = db_session.scalars(stmt).all()
4076
items = [schema.Tag.model_validate(db_tag) for db_tag in db_tags]
4177

papermerge/core/features/tags/router.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from papermerge.core.features.tags.db import api as tags_dbapi
1616
from papermerge.core.features.tags import schema as tags_schema
1717
from papermerge.core.exceptions import EntityNotFound
18-
18+
from .types import PaginatedQueryParams
1919

2020
router = APIRouter(
2121
prefix="/tags",
@@ -48,7 +48,7 @@ def retrieve_tags(
4848
user: Annotated[
4949
usr_schema.User, Security(get_current_user, scopes=[scopes.TAG_VIEW])
5050
],
51-
params: CommonQueryParams = Depends(),
51+
params: PaginatedQueryParams = Depends(),
5252
):
5353
"""Retrieves (paginated) tags of the current user
5454
@@ -60,6 +60,8 @@ def retrieve_tags(
6060
user_id=user.id,
6161
page_number=params.page_number,
6262
page_size=params.page_size,
63+
order_by=params.order_by,
64+
filter=params.filter,
6365
)
6466

6567
return tags
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from enum import Enum
2+
3+
from papermerge.core.types import PaginatedQueryParams as BaseParams
4+
5+
6+
class OrderBy(str, Enum):
7+
name_asc = "name"
8+
name_desc = "-name"
9+
pinned_asc = "pinned"
10+
pinned_desc = "-pinned"
11+
description_asc = "description"
12+
description_desc = "-description"
13+
id_asc = "ID"
14+
id_desc = "-ID"
15+
16+
17+
class PaginatedQueryParams(BaseParams):
18+
order_by: OrderBy | None = None

ui2/src/features/tags/apiSlice.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {apiSlice} from "@/features/api/slice"
22
import type {
33
ColoredTag,
4-
NewColoredTag,
54
ColoredTagUpdate,
5+
NewColoredTag,
66
Paginated,
77
PaginatedArgs
88
} from "@/types"
@@ -17,9 +17,21 @@ export const apiSliceWithTags = apiSlice.injectEndpoints({
1717
>({
1818
query: ({
1919
page_number = 1,
20-
page_size = PAGINATION_DEFAULT_ITEMS_PER_PAGES
21-
}: PaginatedArgs) =>
22-
`/tags/?page_number=${page_number}&page_size=${page_size}`,
20+
page_size = PAGINATION_DEFAULT_ITEMS_PER_PAGES,
21+
sort_by = "name",
22+
filter = undefined
23+
}: PaginatedArgs) => {
24+
let ret
25+
26+
if (filter) {
27+
ret = `/tags/?page_number=${page_number}&page_size=${page_size}&order_by=${sort_by}`
28+
ret += `&filter=${filter}`
29+
} else {
30+
ret = `/tags/?page_number=${page_number}&page_size=${page_size}&order_by=${sort_by}`
31+
}
32+
33+
return ret
34+
},
2335
providesTags: (
2436
result = {page_number: 1, page_size: 1, num_pages: 1, items: []},
2537
_error,
Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
1+
import QuickFilter from "@/components/QuickFilter"
2+
import {selectFilterText, selectSelectedIds} from "@/features/tags/tagsSlice"
3+
import {Group, Loader} from "@mantine/core"
14
import {useSelector} from "react-redux"
2-
import {Group} from "@mantine/core"
3-
import {selectSelectedIds} from "@/features/tags/tagsSlice"
4-
import NewButton from "./NewButton"
5-
import EditButton from "./EditButton"
65
import {DeleteTagsButton} from "./DeleteButton"
6+
import EditButton from "./EditButton"
7+
import NewButton from "./NewButton"
8+
9+
interface Args {
10+
isFetching?: boolean
11+
onQuickFilterChange: (value: string) => void
12+
onQuickFilterClear: () => void
13+
}
714

8-
export default function ActionButtons() {
15+
export default function ActionButtons({
16+
isFetching,
17+
onQuickFilterChange,
18+
onQuickFilterClear
19+
}: Args) {
920
const selectedIds = useSelector(selectSelectedIds)
21+
const filterText = useSelector(selectFilterText)
1022

1123
return (
12-
<Group>
13-
<NewButton />
14-
{selectedIds.length == 1 ? <EditButton tagId={selectedIds[0]} /> : ""}
15-
{selectedIds.length >= 1 ? <DeleteTagsButton /> : ""}
24+
<Group justify="space-between">
25+
<Group>
26+
<NewButton />
27+
{selectedIds.length == 1 ? <EditButton tagId={selectedIds[0]} /> : ""}
28+
{selectedIds.length >= 1 ? <DeleteTagsButton /> : ""}
29+
{isFetching && <Loader size={"sm"} />}
30+
</Group>
31+
<Group>
32+
<QuickFilter
33+
onChange={onQuickFilterChange}
34+
onClear={onQuickFilterClear}
35+
filterText={filterText}
36+
/>
37+
</Group>
1638
</Group>
1739
)
1840
}

ui2/src/features/tags/components/List.tsx

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,56 @@
1-
import {useState} from "react"
2-
import {Center, Stack, Table, Checkbox, Loader} from "@mantine/core"
3-
import {useDispatch, useSelector} from "react-redux"
1+
import Th from "@/components/TableSort/Th"
2+
import {useGetPaginatedTagsQuery} from "@/features/tags/apiSlice"
43
import {
5-
selectionAddMany,
6-
selectSelectedIds,
74
clearSelection,
5+
filterUpdated,
6+
lastPageSizeUpdate,
87
selectLastPageSize,
9-
lastPageSizeUpdate
8+
selectReverseSortedByDescription,
9+
selectReverseSortedByID,
10+
selectReverseSortedByName,
11+
selectReverseSortedByPinned,
12+
selectSelectedIds,
13+
selectSortedByDescription,
14+
selectSortedByID,
15+
selectSortedByName,
16+
selectSortedByPinned,
17+
selectTableSortColumns,
18+
selectionAddMany,
19+
sortByUpdated
1020
} from "@/features/tags/tagsSlice"
11-
import {useGetPaginatedTagsQuery} from "@/features/tags/apiSlice"
21+
import {Center, Checkbox, Loader, Stack, Table} from "@mantine/core"
22+
import {useState} from "react"
23+
import {useDispatch, useSelector} from "react-redux"
1224

1325
import Pagination from "@/components/Pagination"
14-
import TagRow from "./TagRow"
26+
import type {TagsListColumnName} from "../types"
1527
import ActionButtons from "./ActionButtons"
28+
import TagRow from "./TagRow"
1629

1730
export default function TagsList() {
1831
const selectedIds = useSelector(selectSelectedIds)
1932
const dispatch = useDispatch()
33+
const tableSortCols = useSelector(selectTableSortColumns)
2034
const lastPageSize = useSelector(selectLastPageSize)
35+
const sortedByName = useSelector(selectSortedByName)
36+
const sortedByPinned = useSelector(selectSortedByPinned)
37+
const sortedByDescription = useSelector(selectSortedByDescription)
38+
const sortedByID = useSelector(selectSortedByID)
39+
const reverseSortedByName = useSelector(selectReverseSortedByName)
40+
const reverseSortedByPinned = useSelector(selectReverseSortedByPinned)
41+
const reverseSortedByDescription = useSelector(
42+
selectReverseSortedByDescription
43+
)
44+
const reverseSortedByID = useSelector(selectReverseSortedByID)
2145

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

2549
const {data, isLoading, isFetching} = useGetPaginatedTagsQuery({
2650
page_number: page,
27-
page_size: pageSize
51+
page_size: pageSize,
52+
sort_by: tableSortCols.sortBy,
53+
filter: tableSortCols.filter
2854
})
2955

3056
const onCheckAll = (checked: boolean) => {
@@ -53,10 +79,27 @@ export default function TagsList() {
5379
}
5480
}
5581

82+
const onSortBy = (columnName: TagsListColumnName) => {
83+
dispatch(sortByUpdated(columnName))
84+
}
85+
86+
const onQuickFilterChange = (value: string) => {
87+
dispatch(filterUpdated(value))
88+
setPage(1)
89+
}
90+
91+
const onQuickFilterClear = () => {
92+
dispatch(filterUpdated(undefined))
93+
setPage(1)
94+
}
95+
5696
if (isLoading || !data) {
5797
return (
5898
<Stack>
59-
<ActionButtons />
99+
<ActionButtons
100+
onQuickFilterChange={onQuickFilterChange}
101+
onQuickFilterClear={onQuickFilterClear}
102+
/>
60103
<Center>
61104
<Loader type="bars" />
62105
</Center>
@@ -67,7 +110,10 @@ export default function TagsList() {
67110
if (data.items.length == 0) {
68111
return (
69112
<div>
70-
<ActionButtons />
113+
<ActionButtons
114+
onQuickFilterChange={onQuickFilterChange}
115+
onQuickFilterClear={onQuickFilterClear}
116+
/>
71117
<Empty />
72118
</div>
73119
)
@@ -77,7 +123,11 @@ export default function TagsList() {
77123

78124
return (
79125
<Stack>
80-
<ActionButtons /> {isFetching && <Loader size={"sm"} />}
126+
<ActionButtons
127+
isFetching={isFetching}
128+
onQuickFilterChange={onQuickFilterChange}
129+
onQuickFilterClear={onQuickFilterClear}
130+
/>
81131
<Table>
82132
<Table.Thead>
83133
<Table.Tr>
@@ -87,10 +137,34 @@ export default function TagsList() {
87137
onChange={e => onCheckAll(e.currentTarget.checked)}
88138
/>
89139
</Table.Th>
90-
<Table.Th>Name</Table.Th>
91-
<Table.Th>Pinned?</Table.Th>
92-
<Table.Th>Description</Table.Th>
93-
<Table.Th>ID</Table.Th>
140+
<Th
141+
sorted={sortedByName}
142+
reversed={reverseSortedByName}
143+
onSort={() => onSortBy("name")}
144+
>
145+
Name
146+
</Th>
147+
<Th
148+
sorted={sortedByPinned}
149+
reversed={reverseSortedByPinned}
150+
onSort={() => onSortBy("pinned")}
151+
>
152+
Pinned?
153+
</Th>
154+
<Th
155+
sorted={sortedByDescription}
156+
reversed={reverseSortedByDescription}
157+
onSort={() => onSortBy("description")}
158+
>
159+
Description
160+
</Th>
161+
<Th
162+
sorted={sortedByID}
163+
reversed={reverseSortedByID}
164+
onSort={() => onSortBy("ID")}
165+
>
166+
ID
167+
</Th>
94168
</Table.Tr>
95169
</Table.Thead>
96170
<Table.Tbody>{tagRows}</Table.Tbody>

0 commit comments

Comments
 (0)