Client - display pagination on workouts list

This commit is contained in:
Sam 2021-11-02 20:26:43 +01:00
parent e8350abf55
commit a8d0680457
16 changed files with 297 additions and 10 deletions

View File

@ -8,16 +8,22 @@
:event="pagination.has_prev ? 'click' : ''" :event="pagination.has_prev ? 'click' : ''"
:disabled="!pagination.has_prev" :disabled="!pagination.has_prev"
> >
<i class="fa fa-chevron-left" aria-hidden="true" />
{{ $t('api.PAGINATION.PREVIOUS') }} {{ $t('api.PAGINATION.PREVIOUS') }}
</router-link> </router-link>
</li> </li>
<li <li
v-for="page in rangePagination(pagination.pages)" v-for="page in rangePagination(pagination.pages, pagination.page)"
:key="page" :key="page"
class="page" class="page"
:class="{ active: page === pagination.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 }} {{ page }}
</router-link> </router-link>
</li> </li>
@ -29,6 +35,7 @@
:disabled="!pagination.has_next" :disabled="!pagination.has_next"
> >
{{ $t('api.PAGINATION.NEXT') }} {{ $t('api.PAGINATION.NEXT') }}
<i class="fa fa-chevron-right" aria-hidden="true" />
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -39,6 +46,7 @@
import { PropType, defineComponent } from 'vue' import { PropType, defineComponent } from 'vue'
import { IPagination, TPaginationPayload } from '@/types/api' import { IPagination, TPaginationPayload } from '@/types/api'
import { rangePagination } from '@/utils/api'
export default defineComponent({ export default defineComponent({
name: 'Pagination', name: 'Pagination',
@ -57,9 +65,6 @@
}, },
}, },
setup(props) { setup(props) {
function rangePagination(pages: number): number[] {
return Array.from({ length: pages }, (_, i) => i + 1)
}
function getQuery(page: number, cursor?: number): TPaginationPayload { function getQuery(page: number, cursor?: number): TPaginationPayload {
const newQuery = Object.assign({}, props.query) const newQuery = Object.assign({}, props.query)
newQuery.page = cursor ? page + cursor : page newQuery.page = cursor ? page + cursor : page
@ -115,6 +120,18 @@
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
margin-left: -1px; 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> </style>

View File

@ -143,7 +143,7 @@
sum += tooltipItem.parsed.y sum += tooltipItem.parsed.y
}) })
return ( return (
`${t('statistics.TOTAL')}: ` + `${t('common.TOTAL')}: ` +
formatTooltipValue(props.displayedData, sum) formatTooltipValue(props.displayedData, sum)
) )
}, },

View File

@ -197,6 +197,9 @@
} }
function onFilter() { function onFilter() {
emit('filter') emit('filter')
if ('page' in params) {
params['page'] = '1'
}
router.push({ path: '/workouts', query: params }) router.push({ path: '/workouts', query: params })
} }

View File

@ -1,6 +1,15 @@
<template> <template>
<div class="workouts-list"> <div class="workouts-list">
<div class="box" :class="{ 'empty-table': workouts.length === 0 }"> <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 <FilterSelects
:sort="sortList" :sort="sortList"
:order_by="orderByList" :order_by="orderByList"
@ -9,6 +18,12 @@
@updateSelect="reloadWorkouts" @updateSelect="reloadWorkouts"
/> />
<div class="workouts-table responsive-table"> <div class="workouts-table responsive-table">
<Pagination
class="top-pagination"
:pagination="pagination"
path="/workouts"
:query="query"
/>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -96,6 +111,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<Pagination :pagination="pagination" path="/workouts" :query="query" />
</div> </div>
</div> </div>
<NoWorkouts v-if="workouts.length === 0" /> <NoWorkouts v-if="workouts.length === 0" />
@ -117,9 +133,11 @@
import { LocationQuery, useRoute, useRouter } from 'vue-router' import { LocationQuery, useRoute, useRouter } from 'vue-router'
import FilterSelects from '@/components/Common/FilterSelects.vue' import FilterSelects from '@/components/Common/FilterSelects.vue'
import Pagination from '@/components/Common/Pagination.vue'
import StaticMap from '@/components/Common/StaticMap.vue' import StaticMap from '@/components/Common/StaticMap.vue'
import NoWorkouts from '@/components/Workouts/NoWorkouts.vue' import NoWorkouts from '@/components/Workouts/NoWorkouts.vue'
import { WORKOUTS_STORE } from '@/store/constants' import { WORKOUTS_STORE } from '@/store/constants'
import { IPagination } from '@/types/api'
import { ITranslatedSport } from '@/types/sports' import { ITranslatedSport } from '@/types/sports'
import { IUserProfile } from '@/types/user' import { IUserProfile } from '@/types/user'
import { IWorkout, TWorkoutsPayload } from '@/types/workouts' import { IWorkout, TWorkoutsPayload } from '@/types/workouts'
@ -133,6 +151,7 @@
components: { components: {
FilterSelects, FilterSelects,
NoWorkouts, NoWorkouts,
Pagination,
StaticMap, StaticMap,
}, },
props: { props: {
@ -158,6 +177,9 @@
const workouts: ComputedRef<IWorkout[]> = computed( const workouts: ComputedRef<IWorkout[]> = computed(
() => store.getters[WORKOUTS_STORE.GETTERS.USER_WORKOUTS] () => 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) let query: TWorkoutsPayload = getWorkoutsQuery(route.query)
onBeforeMount(() => { onBeforeMount(() => {
@ -205,6 +227,7 @@
return { return {
query, query,
orderByList, orderByList,
pagination,
sortList, sortList,
workouts, workouts,
capitalize, capitalize,
@ -225,11 +248,36 @@
width: 100%; width: 100%;
.box { .box {
padding: $default-padding $default-padding * 2;
@media screen and (max-width: $small-limit) { @media screen and (max-width: $small-limit) {
&.empty-table { &.empty-table {
display: none; 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 { .workouts-table {
.sport-col { .sport-col {
padding-right: 0; padding-right: 0;

View File

@ -15,5 +15,6 @@
"PER_PAGE": { "PER_PAGE": {
"LABEL": "par page" "LABEL": "par page"
} }
} },
"TOTAL": "Total"
} }

View File

@ -1,6 +1,5 @@
{ {
"STATISTICS": "Statistics", "STATISTICS": "Statistics",
"TOTAL": "Total",
"TIME_FRAMES": { "TIME_FRAMES": {
"week": "week", "week": "week",
"month": "month", "month": "month",

View File

@ -15,5 +15,6 @@
"PER_PAGE": { "PER_PAGE": {
"LABEL": "par page" "LABEL": "par page"
} }
} },
"TOTAL ": "Total"
} }

View File

@ -1,6 +1,5 @@
{ {
"STATISTICS": "Statistiques", "STATISTICS": "Statistiques",
"TOTAL": "Total",
"TIME_FRAMES": { "TIME_FRAMES": {
"week": "semaine", "week": "semaine",
"month": "mois", "month": "mois",

View File

@ -30,6 +30,12 @@ const getWorkouts = (
.then((res) => { .then((res) => {
if (res.data.status === 'success') { if (res.data.status === 'success') {
context.commit(WORKOUTS_STORE.MUTATIONS[target], res.data.data.workouts) 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 { } else {
handleError(context, null) handleError(context, null)
} }

View File

@ -15,6 +15,7 @@ export enum WorkoutsGetters {
TIMELINE_WORKOUTS = 'TIMELINE_WORKOUTS', TIMELINE_WORKOUTS = 'TIMELINE_WORKOUTS',
USER_WORKOUTS = 'USER_WORKOUTS', USER_WORKOUTS = 'USER_WORKOUTS',
WORKOUT_DATA = 'WORKOUT_DATA', WORKOUT_DATA = 'WORKOUT_DATA',
WORKOUTS_PAGINATION = 'WORKOUTS_PAGINATION',
} }
export enum WorkoutsMutations { export enum WorkoutsMutations {
@ -29,4 +30,5 @@ export enum WorkoutsMutations {
SET_WORKOUT_GPX = 'SET_WORKOUT_GPX', SET_WORKOUT_GPX = 'SET_WORKOUT_GPX',
SET_WORKOUT_CHART_DATA = 'SET_WORKOUT_CHART_DATA', SET_WORKOUT_CHART_DATA = 'SET_WORKOUT_CHART_DATA',
SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING', SET_WORKOUT_LOADING = 'SET_WORKOUT_LOADING',
SET_WORKOUTS_PAGINATION = 'SET_WORKOUTS_PAGINATION',
} }

View File

@ -21,4 +21,7 @@ export const getters: GetterTree<IWorkoutsState, IRootState> &
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA]: (state: IWorkoutsState) => { [WORKOUTS_STORE.GETTERS.WORKOUT_DATA]: (state: IWorkoutsState) => {
return state.workoutData return state.workoutData
}, },
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION]: (state: IWorkoutsState) => {
return state.pagination
},
} }

View File

@ -5,6 +5,7 @@ import {
IWorkoutsState, IWorkoutsState,
TWorkoutsMutations, TWorkoutsMutations,
} from '@/store/modules/workouts/types' } from '@/store/modules/workouts/types'
import { IPagination } from '@/types/api'
import { IWorkout, IWorkoutApiChartData } from '@/types/workouts' import { IWorkout, IWorkoutApiChartData } from '@/types/workouts'
export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = { export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
@ -32,6 +33,12 @@ export const mutations: MutationTree<IWorkoutsState> & TWorkoutsMutations = {
) { ) {
state.user_workouts = workouts state.user_workouts = workouts
}, },
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUTS_PAGINATION](
state: IWorkoutsState,
pagination: IPagination
) {
state.pagination = pagination
},
[WORKOUTS_STORE.MUTATIONS.SET_WORKOUT]( [WORKOUTS_STORE.MUTATIONS.SET_WORKOUT](
state: IWorkoutsState, state: IWorkoutsState,
workout: IWorkout workout: IWorkout

View File

@ -1,9 +1,11 @@
import { IWorkoutsState } from '@/store/modules/workouts/types' import { IWorkoutsState } from '@/store/modules/workouts/types'
import { IPagination } from '@/types/api'
import { IWorkout } from '@/types/workouts' import { IWorkout } from '@/types/workouts'
export const workoutsState: IWorkoutsState = { export const workoutsState: IWorkoutsState = {
calendar_workouts: [], calendar_workouts: [],
timeline_workouts: [], timeline_workouts: [],
pagination: <IPagination>{},
user_workouts: [], user_workouts: [],
workoutData: { workoutData: {
gpx: '', gpx: '',

View File

@ -7,6 +7,7 @@ import {
import { WORKOUTS_STORE } from '@/store/constants' import { WORKOUTS_STORE } from '@/store/constants'
import { IRootState } from '@/store/modules/root/types' import { IRootState } from '@/store/modules/root/types'
import { IPagination } from '@/types/api'
import { import {
IWorkout, IWorkout,
IWorkoutApiChartData, IWorkoutApiChartData,
@ -21,6 +22,7 @@ export interface IWorkoutsState {
calendar_workouts: IWorkout[] calendar_workouts: IWorkout[]
timeline_workouts: IWorkout[] timeline_workouts: IWorkout[]
workoutData: IWorkoutData workoutData: IWorkoutData
pagination: IPagination
} }
export interface IWorkoutsActions { export interface IWorkoutsActions {
@ -67,6 +69,9 @@ export interface IWorkoutsGetters {
[WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS](state: IWorkoutsState): IWorkout[] [WORKOUTS_STORE.GETTERS.TIMELINE_WORKOUTS](state: IWorkoutsState): IWorkout[]
[WORKOUTS_STORE.GETTERS.USER_WORKOUTS](state: IWorkoutsState): IWorkout[] [WORKOUTS_STORE.GETTERS.USER_WORKOUTS](state: IWorkoutsState): IWorkout[]
[WORKOUTS_STORE.GETTERS.WORKOUT_DATA](state: IWorkoutsState): IWorkoutData [WORKOUTS_STORE.GETTERS.WORKOUT_DATA](state: IWorkoutsState): IWorkoutData
[WORKOUTS_STORE.GETTERS.WORKOUTS_PAGINATION](
state: IWorkoutsState
): IPagination
} }
export type TWorkoutsMutations<S = IWorkoutsState> = { export type TWorkoutsMutations<S = IWorkoutsState> = {
@ -96,6 +101,10 @@ export type TWorkoutsMutations<S = IWorkoutsState> = {
state: S, state: S,
loading: boolean loading: boolean
): void ): 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_CALENDAR_WORKOUTS](state: S): void
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: S): void [WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUTS](state: S): void
[WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: S): void [WORKOUTS_STORE.MUTATIONS.EMPTY_WORKOUT](state: S): void

View File

@ -62,3 +62,55 @@ export const workoutsPayloadKeys = [
'duration_to', 'duration_to',
'sport_id', '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
}

View File

@ -7,6 +7,7 @@ import {
getNumberQueryValue, getNumberQueryValue,
getStringQueryValue, getStringQueryValue,
getQuery, getQuery,
rangePagination,
} from '@/utils/api' } from '@/utils/api'
const orderByList = ['admin', 'created_at', 'username', 'workouts_count'] 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
)
})
})
})