feat(pagination): paginate activity and progress lists
This commit is contained in:
@@ -407,7 +407,10 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wantedDocs, err := api.db.Queries.GetWantedDocuments(c, string(jsonHaves))
|
wantedDocs, err := api.db.Queries.GetWantedDocuments(c, database.GetWantedDocumentsParams{
|
||||||
|
JsonEach: string(jsonHaves),
|
||||||
|
DocumentIds: string(jsonHaves),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetWantedDocuments DB Error", err)
|
log.Error("GetWantedDocuments DB Error", err)
|
||||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
|
|||||||
documentID = *request.Params.DocumentId
|
documentID = *request.Params.DocumentId
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := int64(0)
|
page := int64(1)
|
||||||
if request.Params.Offset != nil {
|
if request.Params.Page != nil {
|
||||||
offset = *request.Params.Offset
|
page = *request.Params.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := int64(100)
|
limit := int64(25)
|
||||||
if request.Params.Limit != nil {
|
if request.Params.Limit != nil {
|
||||||
limit = *request.Params.Limit
|
limit = *request.Params.Limit
|
||||||
}
|
}
|
||||||
@@ -39,13 +39,33 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
|
|||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
DocFilter: docFilter,
|
DocFilter: docFilter,
|
||||||
DocumentID: documentID,
|
DocumentID: documentID,
|
||||||
Offset: offset,
|
Offset: (page - 1) * limit,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return GetActivity500JSONResponse{Code: 500, Message: err.Error()}, nil
|
return GetActivity500JSONResponse{Code: 500, Message: err.Error()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get Total Count
|
||||||
|
total, err := s.db.Queries.GetActivityCount(ctx, database.GetActivityCountParams{
|
||||||
|
UserID: auth.UserName,
|
||||||
|
DocFilter: docFilter,
|
||||||
|
DocumentID: documentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return GetActivity500JSONResponse{Code: 500, Message: err.Error()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Pagination
|
||||||
|
var nextPage *int64
|
||||||
|
var previousPage *int64
|
||||||
|
if page*limit < total {
|
||||||
|
nextPage = ptrOf(page + 1)
|
||||||
|
}
|
||||||
|
if page > 1 {
|
||||||
|
previousPage = ptrOf(page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
apiActivities := make([]Activity, len(activities))
|
apiActivities := make([]Activity, len(activities))
|
||||||
for i, a := range activities {
|
for i, a := range activities {
|
||||||
// Convert StartTime from interface{} to string
|
// Convert StartTime from interface{} to string
|
||||||
@@ -70,7 +90,12 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
|
|||||||
}
|
}
|
||||||
|
|
||||||
response := ActivityResponse{
|
response := ActivityResponse{
|
||||||
Activities: apiActivities,
|
Activities: apiActivities,
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
Total: total,
|
||||||
|
NextPage: nextPage,
|
||||||
|
PreviousPage: previousPage,
|
||||||
}
|
}
|
||||||
return GetActivity200JSONResponse(response), nil
|
return GetActivity200JSONResponse(response), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,12 @@ type Activity struct {
|
|||||||
|
|
||||||
// ActivityResponse defines model for ActivityResponse.
|
// ActivityResponse defines model for ActivityResponse.
|
||||||
type ActivityResponse struct {
|
type ActivityResponse struct {
|
||||||
Activities []Activity `json:"activities"`
|
Activities []Activity `json:"activities"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
NextPage *int64 `json:"next_page,omitempty"`
|
||||||
|
Page int64 `json:"page"`
|
||||||
|
PreviousPage *int64 `json:"previous_page,omitempty"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupType defines model for BackupType.
|
// BackupType defines model for BackupType.
|
||||||
@@ -470,7 +475,7 @@ type UsersResponse struct {
|
|||||||
type GetActivityParams struct {
|
type GetActivityParams struct {
|
||||||
DocFilter *bool `form:"doc_filter,omitempty" json:"doc_filter,omitempty"`
|
DocFilter *bool `form:"doc_filter,omitempty" json:"doc_filter,omitempty"`
|
||||||
DocumentId *string `form:"document_id,omitempty" json:"document_id,omitempty"`
|
DocumentId *string `form:"document_id,omitempty" json:"document_id,omitempty"`
|
||||||
Offset *int64 `form:"offset,omitempty" json:"offset,omitempty"`
|
Page *int64 `form:"page,omitempty" json:"page,omitempty"`
|
||||||
Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"`
|
Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,11 +745,11 @@ func (siw *ServerInterfaceWrapper) GetActivity(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------- Optional query parameter "offset" -------------
|
// ------------- Optional query parameter "page" -------------
|
||||||
|
|
||||||
err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"})
|
err = runtime.BindQueryParameterWithOptions("form", true, false, "page", r.URL.Query(), ¶ms.Page, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err})
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "page", Err: err})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ func (s *Server) GetDocuments(ctx context.Context, request GetDocumentsRequestOb
|
|||||||
limit = *request.Params.Limit
|
limit = *request.Params.Limit
|
||||||
}
|
}
|
||||||
|
|
||||||
search := ""
|
var search *string
|
||||||
if request.Params.Search != nil {
|
if request.Params.Search != nil && *request.Params.Search != "" {
|
||||||
search = "%" + *request.Params.Search + "%"
|
search = ptrOf("%" + *request.Params.Search + "%")
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.db.Queries.GetDocumentsWithStats(
|
rows, err := s.db.Queries.GetDocumentsWithStats(
|
||||||
ctx,
|
ctx,
|
||||||
database.GetDocumentsWithStatsParams{
|
database.GetDocumentsWithStatsParams{
|
||||||
UserID: auth.UserName,
|
UserID: auth.UserName,
|
||||||
Query: &search,
|
Query: search,
|
||||||
Deleted: ptrOf(false),
|
Deleted: ptrOf(false),
|
||||||
Offset: (page - 1) * limit,
|
Offset: (page - 1) * limit,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
@@ -52,7 +52,19 @@ func (s *Server) GetDocuments(ctx context.Context, request GetDocumentsRequestOb
|
|||||||
return GetDocuments500JSONResponse{Code: 500, Message: err.Error()}, nil
|
return GetDocuments500JSONResponse{Code: 500, Message: err.Error()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
total := int64(len(rows))
|
// Get Total Count
|
||||||
|
total, err := s.db.Queries.GetDocumentsWithStatsCount(
|
||||||
|
ctx,
|
||||||
|
database.GetDocumentsWithStatsCountParams{
|
||||||
|
Query: search,
|
||||||
|
Deleted: ptrOf(false),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return GetDocuments500JSONResponse{Code: 500, Message: err.Error()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Pagination
|
||||||
var nextPage *int64
|
var nextPage *int64
|
||||||
var previousPage *int64
|
var previousPage *int64
|
||||||
if page*limit < total {
|
if page*limit < total {
|
||||||
@@ -219,7 +231,6 @@ func (s *Server) EditDocument(ctx context.Context, request EditDocumentRequestOb
|
|||||||
|
|
||||||
doc := docs[0]
|
doc := docs[0]
|
||||||
|
|
||||||
|
|
||||||
apiDoc := Document{
|
apiDoc := Document{
|
||||||
Id: doc.ID,
|
Id: doc.ID,
|
||||||
Title: *doc.Title,
|
Title: *doc.Title,
|
||||||
@@ -561,7 +572,6 @@ func (s *Server) UploadDocumentCover(ctx context.Context, request UploadDocument
|
|||||||
|
|
||||||
doc := docs[0]
|
doc := docs[0]
|
||||||
|
|
||||||
|
|
||||||
apiDoc := Document{
|
apiDoc := Document{
|
||||||
Id: doc.ID,
|
Id: doc.ID,
|
||||||
Title: *doc.Title,
|
Title: *doc.Title,
|
||||||
|
|||||||
@@ -350,8 +350,26 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Activity'
|
$ref: '#/components/schemas/Activity'
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
limit:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
next_page:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
previous_page:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
required:
|
required:
|
||||||
- activities
|
- activities
|
||||||
|
- page
|
||||||
|
- limit
|
||||||
|
- total
|
||||||
|
|
||||||
Device:
|
Device:
|
||||||
type: object
|
type: object
|
||||||
@@ -1174,18 +1192,18 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
- name: offset
|
- name: page
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
default: 0
|
default: 1
|
||||||
- name: limit
|
- name: limit
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
default: 100
|
default: 25
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package v1
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -43,13 +42,21 @@ func (s *Server) GetProgressList(ctx context.Context, request GetProgressListReq
|
|||||||
return GetProgressList500JSONResponse{Code: 500, Message: "Database error"}, nil
|
return GetProgressList500JSONResponse{Code: 500, Message: "Database error"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
total := int64(len(progress))
|
// Get Total Count
|
||||||
|
total, err := s.db.Queries.GetProgressCount(ctx, database.GetProgressCountParams{
|
||||||
|
UserID: auth.UserName,
|
||||||
|
DocFilter: filter.DocFilter,
|
||||||
|
DocumentID: filter.DocumentID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("GetProgressCount DB Error:", err)
|
||||||
|
return GetProgressList500JSONResponse{Code: 500, Message: "Database error"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Pagination
|
||||||
var nextPage *int64
|
var nextPage *int64
|
||||||
var previousPage *int64
|
var previousPage *int64
|
||||||
|
if page*limit < total {
|
||||||
// Calculate total pages
|
|
||||||
totalPages := int64(math.Ceil(float64(total) / float64(limit)))
|
|
||||||
if page < totalPages {
|
|
||||||
nextPage = ptrOf(page + 1)
|
nextPage = ptrOf(page + 1)
|
||||||
}
|
}
|
||||||
if page > 1 {
|
if page > 1 {
|
||||||
|
|||||||
@@ -396,3 +396,40 @@ SET
|
|||||||
isbn10 = COALESCE(excluded.isbn10, isbn10),
|
isbn10 = COALESCE(excluded.isbn10, isbn10),
|
||||||
isbn13 = COALESCE(excluded.isbn13, isbn13)
|
isbn13 = COALESCE(excluded.isbn13, isbn13)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetDocumentsWithStatsCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM documents AS docs
|
||||||
|
WHERE
|
||||||
|
(docs.id = sqlc.narg('id') OR $id IS NULL)
|
||||||
|
AND (docs.deleted = sqlc.narg(deleted) OR $deleted IS NULL)
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
docs.title LIKE sqlc.narg('query') OR
|
||||||
|
docs.author LIKE $query
|
||||||
|
) OR $query IS NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: GetProgressCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM document_progress AS progress
|
||||||
|
WHERE
|
||||||
|
progress.user_id = $user_id
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
CAST($doc_filter AS BOOLEAN) = TRUE
|
||||||
|
AND document_id = $document_id
|
||||||
|
) OR $doc_filter = FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: GetActivityCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM activity
|
||||||
|
WHERE
|
||||||
|
activity.user_id = $user_id
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
CAST($doc_filter AS BOOLEAN) = TRUE
|
||||||
|
AND document_id = $document_id
|
||||||
|
) OR $doc_filter = FALSE
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.29.0
|
// sqlc v1.31.1
|
||||||
// source: query.sql
|
// source: query.sql
|
||||||
|
|
||||||
package database
|
package database
|
||||||
@@ -264,6 +264,32 @@ func (q *Queries) GetActivity(ctx context.Context, arg GetActivityParams) ([]Get
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getActivityCount = `-- name: GetActivityCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM activity
|
||||||
|
WHERE
|
||||||
|
activity.user_id = ?1
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
CAST(?2 AS BOOLEAN) = TRUE
|
||||||
|
AND document_id = ?3
|
||||||
|
) OR ?2 = FALSE
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetActivityCountParams struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
DocFilter bool `json:"doc_filter"`
|
||||||
|
DocumentID string `json:"document_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetActivityCount(ctx context.Context, arg GetActivityCountParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getActivityCount, arg.UserID, arg.DocFilter, arg.DocumentID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const getDailyReadStats = `-- name: GetDailyReadStats :many
|
const getDailyReadStats = `-- name: GetDailyReadStats :many
|
||||||
WITH RECURSIVE last_30_days AS (
|
WITH RECURSIVE last_30_days AS (
|
||||||
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
SELECT LOCAL_DATE(STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'), timezone) AS date
|
||||||
@@ -734,6 +760,33 @@ func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWit
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDocumentsWithStatsCount = `-- name: GetDocumentsWithStatsCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM documents AS docs
|
||||||
|
WHERE
|
||||||
|
(docs.id = ?1 OR ?1 IS NULL)
|
||||||
|
AND (docs.deleted = ?2 OR ?2 IS NULL)
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
docs.title LIKE ?3 OR
|
||||||
|
docs.author LIKE ?3
|
||||||
|
) OR ?3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetDocumentsWithStatsCountParams struct {
|
||||||
|
ID *string `json:"id"`
|
||||||
|
Deleted *bool `json:"-"`
|
||||||
|
Query *string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetDocumentsWithStatsCount(ctx context.Context, arg GetDocumentsWithStatsCountParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getDocumentsWithStatsCount, arg.ID, arg.Deleted, arg.Query)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const getLastActivity = `-- name: GetLastActivity :one
|
const getLastActivity = `-- name: GetLastActivity :one
|
||||||
SELECT start_time
|
SELECT start_time
|
||||||
FROM activity
|
FROM activity
|
||||||
@@ -897,6 +950,32 @@ func (q *Queries) GetProgress(ctx context.Context, arg GetProgressParams) ([]Get
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProgressCount = `-- name: GetProgressCount :one
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM document_progress AS progress
|
||||||
|
WHERE
|
||||||
|
progress.user_id = ?1
|
||||||
|
AND (
|
||||||
|
(
|
||||||
|
CAST(?2 AS BOOLEAN) = TRUE
|
||||||
|
AND document_id = ?3
|
||||||
|
) OR ?2 = FALSE
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetProgressCountParams struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
DocFilter bool `json:"doc_filter"`
|
||||||
|
DocumentID string `json:"document_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetProgressCount(ctx context.Context, arg GetProgressCountParams) (int64, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getProgressCount, arg.UserID, arg.DocFilter, arg.DocumentID)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const getUser = `-- name: GetUser :one
|
const getUser = `-- name: GetUser :one
|
||||||
SELECT id, pass, auth_hash, admin, timezone, created_at FROM users
|
SELECT id, pass, auth_hash, admin, timezone, created_at FROM users
|
||||||
WHERE id = ?1 LIMIT 1
|
WHERE id = ?1 LIMIT 1
|
||||||
@@ -1088,17 +1167,22 @@ WHERE (
|
|||||||
AND documents.filepath IS NULL
|
AND documents.filepath IS NULL
|
||||||
)
|
)
|
||||||
OR (documents.id IS NULL)
|
OR (documents.id IS NULL)
|
||||||
OR CAST(?1 AS TEXT) != CAST(?1 AS TEXT)
|
OR CAST(?2 AS TEXT) != CAST(?2 AS TEXT)
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type GetWantedDocumentsParams struct {
|
||||||
|
JsonEach interface{} `json:"json_each"`
|
||||||
|
DocumentIds string `json:"document_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetWantedDocumentsRow struct {
|
type GetWantedDocumentsRow struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
WantFile bool `json:"want_file"`
|
WantFile bool `json:"want_file"`
|
||||||
WantMetadata bool `json:"want_metadata"`
|
WantMetadata bool `json:"want_metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetWantedDocuments(ctx context.Context, documentIds string) ([]GetWantedDocumentsRow, error) {
|
func (q *Queries) GetWantedDocuments(ctx context.Context, arg GetWantedDocumentsParams) ([]GetWantedDocumentsRow, error) {
|
||||||
rows, err := q.db.QueryContext(ctx, getWantedDocuments, documentIds)
|
rows, err := q.db.QueryContext(ctx, getWantedDocuments, arg.JsonEach, arg.DocumentIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
51
frontend/src/components/Pagination.tsx
Normal file
51
frontend/src/components/Pagination.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
interface PaginationProps {
|
||||||
|
page: number;
|
||||||
|
previousPage?: number;
|
||||||
|
nextPage?: number;
|
||||||
|
total?: number;
|
||||||
|
limit?: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Pagination({
|
||||||
|
page,
|
||||||
|
previousPage,
|
||||||
|
nextPage,
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
onPageChange,
|
||||||
|
}: PaginationProps) {
|
||||||
|
if (!previousPage && !nextPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = total && limit ? Math.ceil(total / limit) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 flex w-full items-center justify-center gap-4 text-content">
|
||||||
|
{previousPage && previousPage > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPageChange(previousPage)}
|
||||||
|
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
||||||
|
>
|
||||||
|
◄
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{totalPages ? (
|
||||||
|
<span className="text-sm text-content-muted">
|
||||||
|
Page {page} of {totalPages}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{nextPage && nextPage > 0 ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPageChange(nextPage)}
|
||||||
|
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
||||||
|
>
|
||||||
|
►
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ export {
|
|||||||
InlineLoader,
|
InlineLoader,
|
||||||
} from './Skeleton';
|
} from './Skeleton';
|
||||||
export { LoadingState } from './LoadingState';
|
export { LoadingState } from './LoadingState';
|
||||||
|
export { Pagination } from './Pagination';
|
||||||
|
|
||||||
// Field components
|
// Field components
|
||||||
export { Field, FieldLabel, FieldValue, FieldActions } from './Field';
|
export { Field, FieldLabel, FieldValue, FieldActions } from './Field';
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ import type { Activity } from './activity';
|
|||||||
|
|
||||||
export interface ActivityResponse {
|
export interface ActivityResponse {
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
next_page?: number;
|
||||||
|
previous_page?: number;
|
||||||
|
total: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
export type GetActivityParams = {
|
export type GetActivityParams = {
|
||||||
doc_filter?: boolean;
|
doc_filter?: boolean;
|
||||||
document_id?: string;
|
document_id?: string;
|
||||||
offset?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { useGetActivity } from '../generated/anthoLumeAPIV1';
|
import { useGetActivity } from '../generated/anthoLumeAPIV1';
|
||||||
import type { Activity } from '../generated/model';
|
import type { Activity } from '../generated/model';
|
||||||
|
import { Pagination } from '../components';
|
||||||
import { Table, type Column } from '../components/Table';
|
import { Table, type Column } from '../components/Table';
|
||||||
import { formatDuration } from '../utils/formatters';
|
import { formatDuration } from '../utils/formatters';
|
||||||
|
|
||||||
export default function ActivityPage() {
|
export default function ActivityPage() {
|
||||||
const { data, isLoading } = useGetActivity({ offset: 0, limit: 100 });
|
const [searchParams] = useSearchParams();
|
||||||
const activities = data?.status === 200 ? data.data.activities : [];
|
const documentID = searchParams.get('document') || undefined;
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const limit = 25;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [documentID]);
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetActivity({
|
||||||
|
doc_filter: Boolean(documentID),
|
||||||
|
document_id: documentID,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
const response = data?.status === 200 ? data.data : undefined;
|
||||||
|
const activities = response?.activities ?? [];
|
||||||
|
|
||||||
const columns: Column<Activity>[] = [
|
const columns: Column<Activity>[] = [
|
||||||
{
|
{
|
||||||
@@ -35,5 +52,17 @@ export default function ActivityPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Table columns={columns} data={activities || []} loading={isLoading} />;
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Table columns={columns} data={activities} loading={isLoading} />
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
previousPage={response?.previous_page}
|
||||||
|
nextPage={response?.next_page}
|
||||||
|
total={response?.total}
|
||||||
|
limit={limit}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1';
|
import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1';
|
||||||
import type { Document, DocumentsResponse } from '../generated/model';
|
import type { Document, DocumentsResponse } from '../generated/model';
|
||||||
import { ActivityIcon, DownloadIcon, Search2Icon, UploadIcon } from '../icons';
|
import { ActivityIcon, DownloadIcon, Search2Icon, UploadIcon } from '../icons';
|
||||||
import { LoadingState } from '../components';
|
import { LoadingState, Pagination } from '../components';
|
||||||
import { useToasts } from '../components/ToastContext';
|
import { useToasts } from '../components/ToastContext';
|
||||||
import { formatDuration } from '../utils/formatters';
|
import { formatDuration } from '../utils/formatters';
|
||||||
import { useDebounce } from '../hooks/useDebounce';
|
import { useDebounce } from '../hooks/useDebounce';
|
||||||
@@ -272,24 +272,14 @@ export default function DocumentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-4 flex w-full justify-center gap-4 text-content">
|
<Pagination
|
||||||
{previousPage && previousPage > 0 && (
|
page={page}
|
||||||
<button
|
previousPage={previousPage}
|
||||||
onClick={() => setPage(page - 1)}
|
nextPage={nextPage}
|
||||||
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
total={(data?.data as DocumentsResponse | undefined)?.total}
|
||||||
>
|
limit={limit}
|
||||||
◄
|
onPageChange={setPage}
|
||||||
</button>
|
/>
|
||||||
)}
|
|
||||||
{nextPage && nextPage > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(page + 1)}
|
|
||||||
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
|
|
||||||
>
|
|
||||||
►
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="fixed bottom-6 right-6 flex items-center justify-center rounded-full">
|
<div className="fixed bottom-6 right-6 flex items-center justify-center rounded-full">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useGetProgressList } from '../generated/anthoLumeAPIV1';
|
import { useGetProgressList } from '../generated/anthoLumeAPIV1';
|
||||||
import type { Progress } from '../generated/model';
|
import type { Progress } from '../generated/model';
|
||||||
|
import { Pagination } from '../components';
|
||||||
import { Table, type Column } from '../components/Table';
|
import { Table, type Column } from '../components/Table';
|
||||||
|
|
||||||
export default function ProgressPage() {
|
export default function ProgressPage() {
|
||||||
const { data, isLoading } = useGetProgressList({ page: 1, limit: 15 });
|
const [page, setPage] = useState(1);
|
||||||
const progress = data?.status === 200 ? (data.data.progress ?? []) : [];
|
const limit = 15;
|
||||||
|
const { data, isLoading } = useGetProgressList({ page, limit });
|
||||||
|
const response = data?.status === 200 ? data.data : undefined;
|
||||||
|
const progress = response?.progress ?? [];
|
||||||
|
|
||||||
const columns: Column<Progress>[] = [
|
const columns: Column<Progress>[] = [
|
||||||
{
|
{
|
||||||
@@ -35,5 +40,17 @@ export default function ProgressPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <Table columns={columns} data={progress || []} loading={isLoading} />;
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Table columns={columns} data={progress} loading={isLoading} />
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
previousPage={response?.previous_page}
|
||||||
|
nextPage={response?.next_page}
|
||||||
|
total={response?.total}
|
||||||
|
limit={limit}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user