Client - display pagination on workouts list
This commit is contained in:
parent
e8350abf55
commit
a8d0680457
@ -8,16 +8,22 @@
|
||||
:event="pagination.has_prev ? 'click' : ''"
|
||||
:disabled="!pagination.has_prev"
|
||||
>
|
||||
<i class="fa fa-chevron-left" aria-hidden="true" />
|
||||
{{ $t('api.PAGINATION.PREVIOUS') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-for="page in rangePagination(pagination.pages)"
|
||||
v-for="page in rangePagination(pagination.pages, pagination.page)"
|
||||
:key="page"
|
||||
class="page"
|
||||
:class="{ active: page === pagination.page }"
|
||||
>
|
||||
<router-link class="page-link" :to="{ path, query: getQuery(page) }">
|
||||
<span v-if="page === '...'"> ... </span>
|
||||
<router-link
|
||||
v-else
|
||||
class="page-link"
|
||||
:to="{ path, query: getQuery(+page) }"
|
||||
>
|
||||
{{ page }}
|
||||
</router-link>
|
||||
</li>
|
||||
@ -29,6 +35,7 @@
|
||||
:disabled="!pagination.has_next"
|
||||
>
|
||||
{{ $t('api.PAGINATION.NEXT') }}
|
||||
<i class="fa fa-chevron-right" aria-hidden="true" />
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
@ -39,6 +46,7 @@
|
||||
import { PropType, defineComponent } from 'vue'
|
||||
|
||||
import { IPagination, TPaginationPayload } from '@/types/api'
|
||||
import { rangePagination } from '@/utils/api'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Pagination',
|
||||
@ -57,9 +65,6 @@
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
function rangePagination(pages: number): number[] {
|
||||
return Array.from({ length: pages }, (_, i) => i + 1)
|
||||
}
|
||||
function getQuery(page: number, cursor?: number): TPaginationPayload {
|
||||
const newQuery = Object.assign({}, props.query)
|
||||
newQuery.page = cursor ? page + cursor : page
|
||||
@ -115,6 +120,18 @@
|
||||
border-bottom-right-radius: 5px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
.fa {
|
||||
font-size: 0.8em;
|
||||
padding: 0 $default-padding * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $medium-limit) {
|
||||
.pagination {
|
||||
.page {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -143,7 +143,7 @@
|
||||
sum += tooltipItem.parsed.y
|
||||
})
|
||||
return (
|
||||
`${t('statistics.TOTAL')}: ` +
|
||||
`${t('common.TOTAL')}: ` +
|
||||
formatTooltipValue(props.displayedData, sum)
|
||||
)
|
||||
},
|
||||
|
@ -197,6 +197,9 @@
|
||||
}
|
||||
function onFilter() {
|
||||
emit('filter')
|
||||
if ('page' in params) {
|
||||
params['page'] = '1'
|
||||
}
|
||||
router.push({ path: '/workouts', query: params })
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div class="workouts-list">
|
||||
<div class="box" :class="{ 'empty-table': workouts.length === 0 }">
|
||||
<div class="total">
|
||||
<span class="total-label">
|
||||
{{ $t('common.TOTAL').toLowerCase() }}:
|
||||
</span>
|
||||
<span v-if="pagination.total">
|
||||
{{ pagination.total }}
|
||||
{{ $t('workouts.WORKOUT', pagination.total) }}
|
||||
</span>
|
||||
</div>
|
||||
<FilterSelects
|
||||
:sort="sortList"
|
||||
:order_by="orderByList"
|
||||
@ -9,6 +18,12 @@
|
||||
@updateSelect="reloadWorkouts"
|
||||
/>
|
||||
<div class="workouts-table responsive-table">
|
||||
<Pagination
|
||||
class="top-pagination"
|
||||
:pagination="pagination"
|
||||
path="/workouts"
|
||||
:query="query"
|
||||
/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -96,6 +111,7 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<Pagination :pagination="pagination" path="/workouts" :query="query" />
|
||||
</div>
|
||||
</div>
|
||||
<NoWorkouts v-if="workouts.length === 0" />
|
||||
@ -117,9 +133,11 @@
|
||||
import { LocationQuery, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import FilterSelects from '@/components/Common/FilterSelects.vue'
|
||||
import Pagination from '@/components/Common/Pagination.vue'
|
||||
import StaticMap from '@/components/Common/StaticMap.vue'
|
||||
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
|
||||
import { WORKOUTS_STORE } from '@/store/constants'
|
||||
import { IPagination } from '@/types/api'
|
||||
import { ITranslatedSport } from '@/types/sports'
|
||||
import { IUserProfile } from '@/types/user'
|
||||
import { IWorkout, TWorkoutsPayload } from '@/types/workouts'
|
||||
@ -133,6 +151,7 @@
|
||||
components: {
|
||||
FilterSelects,
|
||||
NoWorkouts,
|
||||
Pagination,
|
||||
StaticMap,
|
||||
},
|
||||
props: {
|
||||
@ -158,6 +177,9 @@
|
||||
const workouts: ComputedRef<IWorkout[]> = computed(
|
||||
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS]
|
||||
)
|
||||
const pagination: ComputedRef<IPagination> = computed(
|
||||
() => store.getters[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]
|
||||
)
|
||||
let query: TWorkoutsPayload = getWorkoutsQuery(route.query)
|
||||
|
||||
onBeforeMount(() => {
|
||||
@ -205,6 +227,7 @@
|
||||
return {
|
||||
query,
|
||||
orderByList,
|
||||
pagination,
|
||||
sortList,
|
||||
workouts,
|
||||
capitalize,
|
||||
@ -225,11 +248,36 @@
|
||||
width: 100%;
|
||||
|
||||
.box {
|
||||
padding: $default-padding $default-padding * 2;
|
||||
@media screen and (max-width: $small-limit) {
|
||||
&.empty-table {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
display: flex;
|
||||
gap: $default-padding * 0.5;
|
||||
.total-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.top-pagination {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: $small-limit) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
::v-deep(.pagination-center) {
|
||||
@media screen and (max-width: $small-limit) {
|
||||
ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workouts-table {
|
||||
.sport-col {
|
||||
padding-right: 0;
|
||||
|
@ -15,5 +15,6 @@
|
||||
"PER_PAGE": {
|
||||
"LABEL": "par page"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TOTAL": "Total"
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"STATISTICS": "Statistics",
|
||||
"TOTAL": "Total",
|
||||
"TIME_FRAMES": {
|
||||
"week": "week",
|
||||
"month": "month",
|
||||
|
@ -15,5 +15,6 @@
|
||||
"PER_PAGE": {
|
||||
"LABEL": "par page"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TOTAL ": "Total"
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"STATISTICS": "Statistiques",
|
||||
"TOTAL": "Total",
|
||||
"TIME_FRAMES": {
|
||||
"week": "semaine",
|
||||
"month": "mois",
|
||||
|
@ -30,6 +30,12 @@ const getWorkouts = (
|
||||
.then((res) => {
|
||||
if (res.data.status === 'success') {
|
||||
context.commit(WORKOUTS_STORE.MUTATIONS[target], res.data.data.workouts)
|
||||
if (target === WorkoutsMutations['SET_USER_WORKOUTS']) {
|
||||
context.commit(
|
||||
WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION,
|
||||
res.data.pagination
|
||||
)
|
||||
}
|
||||
} else {
|
||||
handleError(context, null)
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export enum WorkoutsGetters {
|
||||
TIMELINE_WORKOUTS = 'TIMELINE_WORKOUTS',
|
||||
USER_WORKOUTS = 'USER_WORKOUTS',
|
||||
WORKOUT_DATA = 'WORKOUT_DATA',
|
||||
WORKOUTS_PAGINATION = 'WORKOUTS_PAGINATION',
|
||||
}
|
||||
|
||||
export enum WorkoutsMutations {
|
||||
@ -29,4 +30,5 @@ export enum WorkoutsMutations {
|
||||
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
|
||||
SET_WORKOUT_CHART_DATA = 'SET_WORKOUT_CHART_DATA',
|
||||
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
|
||||
SET_WORKOUTS_PAGINATION = 'SET_WORKOUTS_PAGINATION',
|
||||
}
|
||||
|
@ -21,4 +21,7 @@ export const getters: GetterTree<IWorkoutsState, IRootState> &
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]: (state: IWorkoutsState) => {
|
||||
return state.workoutData
|
||||
},
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]: (state: IWorkoutsState) => {
|
||||
return state.pagination
|
||||
},
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
IWorkoutsState,
|
||||
TWorkoutsMutations,
|
||||
} from '@/store/modules/workouts/types'
|
||||
import { IPagination } from '@/types/api'
|
||||
import { IWorkout, IWorkoutApiChartData } from '@/types/workouts'
|
||||
|
||||
export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
||||
@ -32,6 +33,12 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
|
||||
) {
|
||||
state.user_workouts = workouts
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION](
|
||||
state: IWorkoutsState,
|
||||
pagination: IPagination
|
||||
) {
|
||||
state.pagination = pagination
|
||||
},
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](
|
||||
state: IWorkoutsState,
|
||||
workout: IWorkout
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { IWorkoutsState } from '@/store/modules/workouts/types'
|
||||
import { IPagination } from '@/types/api'
|
||||
import { IWorkout } from '@/types/workouts'
|
||||
|
||||
export const workoutsState: IWorkoutsState = {
|
||||
calendar_workouts: [],
|
||||
timeline_workouts: [],
|
||||
pagination: <IPagination>{},
|
||||
user_workouts: [],
|
||||
workoutData: {
|
||||
gpx: '',
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
|
||||
import { WORKOUTS_STORE } from '@/store/constants'
|
||||
import { IRootState } from '@/store/modules/root/types'
|
||||
import { IPagination } from '@/types/api'
|
||||
import {
|
||||
IWorkout,
|
||||
IWorkoutApiChartData,
|
||||
@ -21,6 +22,7 @@ export interface IWorkoutsState {
|
||||
calendar_workouts: IWorkout[]
|
||||
timeline_workouts: IWorkout[]
|
||||
workoutData: IWorkoutData
|
||||
pagination: IPagination
|
||||
}
|
||||
|
||||
export interface IWorkoutsActions {
|
||||
@ -67,6 +69,9 @@ export interface IWorkoutsGetters {
|
||||
[WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS](state: IWorkoutsState): IWorkout[]
|
||||
[WORKOUTS_STORE.GETTERS.USER_WORKOUTS](state: IWorkoutsState): IWorkout[]
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA](state: IWorkoutsState): IWorkoutData
|
||||
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION](
|
||||
state: IWorkoutsState
|
||||
): IPagination
|
||||
}
|
||||
|
||||
export type TWorkoutsMutations<S = IWorkoutsState> = {
|
||||
@ -96,6 +101,10 @@ export type TWorkoutsMutations<S = IWorkoutsState> = {
|
||||
state: S,
|
||||
loading: boolean
|
||||
): void
|
||||
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION](
|
||||
state: S,
|
||||
pagination: IPagination
|
||||
): void
|
||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_CALENDAR_WORKOUTS](state: S): void
|
||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: S): void
|
||||
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: S): void
|
||||
|
@ -62,3 +62,55 @@ export const workoutsPayloadKeys = [
|
||||
'duration_to',
|
||||
'sport_id',
|
||||
]
|
||||
|
||||
const getRange = (stop: number, start = 1): number[] => {
|
||||
return Array.from({ length: stop - start + 1 }, (_, i) => start + i)
|
||||
}
|
||||
|
||||
export const rangePagination = (
|
||||
pages: number,
|
||||
currentPage: number
|
||||
): (string | number)[] => {
|
||||
if (pages < 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (pages < 9) {
|
||||
return getRange(pages)
|
||||
}
|
||||
|
||||
let pagination: (string | number)[] = [1, 2]
|
||||
if (currentPage < 4) {
|
||||
pagination = pagination.concat([3, 4, 5])
|
||||
} else if (currentPage < 6) {
|
||||
pagination = pagination.concat(getRange(currentPage + 2, 3))
|
||||
} else {
|
||||
pagination = pagination.concat(['...'])
|
||||
if (currentPage < pages - 2) {
|
||||
pagination = pagination.concat(getRange(currentPage + 2, currentPage - 2))
|
||||
}
|
||||
}
|
||||
if (currentPage + 2 <= pages - 2) {
|
||||
pagination = pagination.concat(['...'])
|
||||
pagination = pagination.concat(getRange(pages, pages - 1))
|
||||
} else {
|
||||
if (
|
||||
pagination[pagination.length - 1] !== '...' &&
|
||||
pagination[pagination.length - 1] >= pages - 2 &&
|
||||
pagination[pagination.length - 1] < pages
|
||||
) {
|
||||
pagination = pagination.concat(
|
||||
getRange(pages, +pagination[pagination.length - 1] + 1)
|
||||
)
|
||||
} else {
|
||||
pagination = pagination.concat(
|
||||
getRange(
|
||||
pages,
|
||||
currentPage < pages - 3 ? currentPage + 3 : currentPage - 5
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return pagination
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
getNumberQueryValue,
|
||||
getStringQueryValue,
|
||||
getQuery,
|
||||
rangePagination,
|
||||
} from '@/utils/api'
|
||||
|
||||
const orderByList = ['admin', 'created_at', 'username', 'workouts_count']
|
||||
@ -236,3 +237,140 @@ describe('getQuery w/ default values and input pagination payload', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rangePagination', () => {
|
||||
const testsParams = [
|
||||
{
|
||||
description: 'returns empty array if pages total equals 0',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: 0,
|
||||
},
|
||||
expectedPagination: [],
|
||||
},
|
||||
{
|
||||
description: 'returns empty array if pages total is a negative value',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: -1,
|
||||
},
|
||||
expectedPagination: [],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 1 and pages total equals 1',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: 1,
|
||||
},
|
||||
expectedPagination: [1],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 1 and pages total equals 4',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: 4,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 4 and pages total equals 8',
|
||||
input: {
|
||||
currentPage: 4,
|
||||
pages: 8,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
},
|
||||
{
|
||||
description: 'returns pagination if current page is 1 and total pages 10',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: 10,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4, 5, '...', 9, 10],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 4 and pages total equals 10',
|
||||
input: {
|
||||
currentPage: 4,
|
||||
pages: 10,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4, 5, 6, '...', 9, 10],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 7 and pages total equals 10',
|
||||
input: {
|
||||
currentPage: 7,
|
||||
pages: 10,
|
||||
},
|
||||
expectedPagination: [1, 2, '...', 5, 6, 7, 8, 9, 10],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 20 and pages total equals 30',
|
||||
input: {
|
||||
currentPage: 20,
|
||||
pages: 30,
|
||||
},
|
||||
expectedPagination: [1, 2, '...', 18, 19, 20, 21, 22, '...', 29, 30],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 1 and total pages 100',
|
||||
input: {
|
||||
currentPage: 1,
|
||||
pages: 100,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4, 5, '...', 99, 100],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 5 and total pages 100',
|
||||
input: {
|
||||
currentPage: 5,
|
||||
pages: 100,
|
||||
},
|
||||
expectedPagination: [1, 2, 3, 4, 5, 6, 7, '...', 99, 100],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 50 and total pages 100',
|
||||
input: {
|
||||
currentPage: 50,
|
||||
pages: 100,
|
||||
},
|
||||
expectedPagination: [1, 2, '...', 48, 49, 50, 51, 52, '...', 99, 100],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 97 and total pages 100',
|
||||
input: {
|
||||
currentPage: 97,
|
||||
pages: 100,
|
||||
},
|
||||
expectedPagination: [1, 2, '...', 95, 96, 97, 98, 99, 100],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'returns pagination if current page is 100 and total pages 100',
|
||||
input: {
|
||||
currentPage: 100,
|
||||
pages: 100,
|
||||
},
|
||||
expectedPagination: [1, 2, '...', 95, 96, 97, 98, 99, 100],
|
||||
},
|
||||
]
|
||||
|
||||
testsParams.map((testParams) => {
|
||||
it(testParams.description, () => {
|
||||
assert.deepEqual(
|
||||
rangePagination(testParams.input.pages, testParams.input.currentPage),
|
||||
testParams.expectedPagination
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user