Client - display info and errors on workout edition

This commit is contained in:
Sam 2021-09-29 16:34:48 +02:00
parent 6bfcb24133
commit 997469d959
10 changed files with 182 additions and 28 deletions

View File

@ -9,7 +9,7 @@
<form @submit.prevent="updateWorkout"> <form @submit.prevent="updateWorkout">
<div class="form-items"> <div class="form-items">
<div class="form-item-radio" v-if="isCreation"> <div class="form-item-radio" v-if="isCreation">
<div class="radio"> <div>
<input <input
id="withGpx" id="withGpx"
type="radio" type="radio"
@ -19,7 +19,7 @@
/> />
<label for="withGpx">{{ t('workouts.WITH_GPX') }}</label> <label for="withGpx">{{ t('workouts.WITH_GPX') }}</label>
</div> </div>
<div class="radio"> <div>
<input <input
id="withoutGpx" id="withoutGpx"
type="radio" type="radio"
@ -52,13 +52,7 @@
<div class="form-item" v-if="isCreation && withGpx"> <div class="form-item" v-if="isCreation && withGpx">
<label for="gpxFile"> <label for="gpxFile">
{{ t('workouts.GPX_FILE') }} {{ t('workouts.GPX_FILE') }}
<sup> {{ t('workouts.ZIP_ARCHIVE_DESCRIPTION') }}:
<i class="fa fa-question-circle" aria-hidden="true" />
</sup>
{{ t('workouts.ZIP_FILE') }}
<sup
><i class="fa fa-question-circle" aria-hidden="true" /></sup
>:
</label> </label>
<input <input
id="gpxFile" id="gpxFile"
@ -68,6 +62,25 @@
:disabled="loading" :disabled="loading"
@input="updateFile" @input="updateFile"
/> />
<div class="files-help">
<div>
<strong>{{ t('workouts.GPX_FILE') }}:</strong>
<ul>
<li>{{ t('workouts.MAX_SIZE') }}: {{ fileSizeLimit }}</li>
</ul>
</div>
<div>
<strong>{{ t('workouts.ZIP_ARCHIVE') }}:</strong>
<ul>
<li>{{ t('workouts.NO_FOLDER') }}</li>
<li>
{{ t('workouts.MAX_FILES') }}: {{ gpx_limit_import }}
</li>
<li>{{ t('workouts.MAX_SIZE') }}: {{ zipSizeLimit }}</li>
</ul>
</div>
</div>
</div> </div>
<div class="form-item" v-else> <div class="form-item" v-else>
<label for="title"> {{ t('workouts.TITLE') }}: </label> <label for="title"> {{ t('workouts.TITLE') }}: </label>
@ -206,11 +219,13 @@
import ErrorMessage from '@/components/Common/ErrorMessage.vue' import ErrorMessage from '@/components/Common/ErrorMessage.vue'
import Loader from '@/components/Common/Loader.vue' import Loader from '@/components/Common/Loader.vue'
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants' import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
import { IAppConfig } from '@/types/application'
import { ISport } from '@/types/sports' import { ISport } from '@/types/sports'
import { IAuthUserProfile } from '@/types/user' import { IAuthUserProfile } from '@/types/user'
import { IWorkout, IWorkoutForm } from '@/types/workouts' import { IWorkout, IWorkoutForm } from '@/types/workouts'
import { useStore } from '@/use/useStore' import { useStore } from '@/use/useStore'
import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates' import { formatWorkoutDate, getDateWithTZ } from '@/utils/dates'
import { getReadableFileSize } from '@/utils/files'
import { translateSports } from '@/utils/sports' import { translateSports } from '@/utils/sports'
export default defineComponent({ export default defineComponent({
@ -257,6 +272,16 @@
const translatedSports: ComputedRef<ISport[]> = computed(() => const translatedSports: ComputedRef<ISport[]> = computed(() =>
translateSports(props.sports, t) translateSports(props.sports, t)
) )
const appConfig: ComputedRef<IAppConfig> = computed(
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
)
const fileSizeLimit = appConfig.value.max_single_file_size
? getReadableFileSize(appConfig.value.max_single_file_size)
: ''
const gpx_limit_import = appConfig.value.gpx_limit_import
const zipSizeLimit = appConfig.value.max_zip_file_size
? getReadableFileSize(appConfig.value.max_zip_file_size)
: ''
const errorMessages: ComputedRef<string | string[] | null> = computed( const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES] () => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
) )
@ -374,10 +399,14 @@
onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)) onUnmounted(() => store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES))
return { return {
appConfig,
errorMessages, errorMessages,
fileSizeLimit,
gpx_limit_import,
t, t,
translatedSports, translatedSports,
withGpx, withGpx,
zipSizeLimit,
workoutDataObject: workoutForm, workoutDataObject: workoutForm,
onCancel, onCancel,
updateFile, updateFile,
@ -450,15 +479,16 @@
.form-item-radio { .form-item-radio {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
.radio { label {
label { font-weight: normal;
font-weight: normal; @media screen and (max-width: $small-limit) {
} font-size: 0.9em;
input {
margin-top: -2px;
vertical-align: middle;
} }
} }
input {
margin-top: -2px;
vertical-align: middle;
}
} }
} }
@ -469,6 +499,29 @@
margin: $default-padding * 0.5; margin: $default-padding * 0.5;
} }
} }
.files-help {
display: flex;
justify-content: space-around;
background-color: var(--info-background-color);
border-radius: $border-radius;
color: var(--info-color);
font-size: 0.8em;
margin-top: $default-margin;
padding: $default-padding;
div {
display: flex;
@media screen and (max-width: $small-limit) {
flex-direction: column;
}
ul {
margin: 0;
padding: 0 $default-padding * 2;
}
}
}
} }
} }
} }

View File

@ -2,10 +2,18 @@
"ERROR": { "ERROR": {
"UNKNOWN": "Error. Please try again or contact the administrator.", "UNKNOWN": "Error. Please try again or contact the administrator.",
"Error, Please try again or contact the administrator": "Error. Please try again or contact the administrator.", "Error, Please try again or contact the administrator": "Error. Please try again or contact the administrator.",
"File extension not allowed": "File extension not allowed.",
"File size is greater than the allowed size": "File size is greater than the allowed size.",
"Invalid credentials": "Invalid credentials.", "Invalid credentials": "Invalid credentials.",
"Network Error": "Network Error", "Invalid payload": "Invalid data.",
"Invalid token, Please log in again": "Invalid token. Please log in again.",
"Network Error": "Network Error.",
"No file part": "No file provided.",
"No selected file": "No selected file.",
"Provide a valid auth token": "Provide a valid auth token.",
"Password and password confirmation don't match": "Password and password confirmation don't match.", "Password and password confirmation don't match": "Password and password confirmation don't match.",
"Password: 8 characters required": "Password: 8 characters required.", "Password: 8 characters required": "Password: 8 characters required.",
"Signature expired, Please log in again": "Signature expired. Please log in again.",
"Username: 3 to 12 characters required": "Username: 3 to 12 characters required.", "Username: 3 to 12 characters required": "Username: 3 to 12 characters required.",
"Valid email must be provided": "Valid email must be provided." "Valid email must be provided": "Valid email must be provided."
} }

View File

@ -10,16 +10,19 @@
"EDIT_WORKOUT": "Edit the workout", "EDIT_WORKOUT": "Edit the workout",
"ELEVATION": "elevation", "ELEVATION": "elevation",
"END": "end", "END": "end",
"GPX_FILE": "fichier .gpx", "GPX_FILE": ".gpx file",
"KM": "km", "KM": "km",
"LATEST_WORKOUTS": "Latest workouts", "LATEST_WORKOUTS": "Latest workouts",
"LOAD_MORE_WORKOUT": "Load more workouts", "LOAD_MORE_WORKOUT": "Load more workouts",
"MAX_ALTITUDE": "max. altitude", "MAX_ALTITUDE": "max. altitude",
"MAX_FILES": "max files",
"MAX_SIZE": "max size",
"MAX_SPEED": "max. speed", "MAX_SPEED": "max. speed",
"MIN_ALTITUDE": "min. altitude", "MIN_ALTITUDE": "min. altitude",
"NEXT_SEGMENT": "No next segment", "NEXT_SEGMENT": "No next segment",
"NEXT_WORKOUT": "Next workout", "NEXT_WORKOUT": "Next workout",
"NO_DATA_CLEANING": "data from gpx, without any cleaning", "NO_DATA_CLEANING": "data from gpx, without any cleaning",
"NO_FOLDER": "no folder inside",
"NO_MAP": "No map", "NO_MAP": "No map",
"NO_NEXT_SEGMENT": "No next segment", "NO_NEXT_SEGMENT": "No next segment",
"NO_NEXT_WORKOUT": "No next workout", "NO_NEXT_WORKOUT": "No next workout",
@ -66,5 +69,6 @@
"WORKOUT": "workout | workouts", "WORKOUT": "workout | workouts",
"WORKOUT_DATE": "workout date", "WORKOUT_DATE": "workout date",
"WORKOUT_DELETION_CONFIRMATION": "Are you sure you want to delete this workout?", "WORKOUT_DELETION_CONFIRMATION": "Are you sure you want to delete this workout?",
"ZIP_FILE": "or .zip file containing .gpx files" "ZIP_ARCHIVE": ".zip file",
"ZIP_ARCHIVE_DESCRIPTION": "or .zip file containing .gpx files"
} }

View File

@ -2,10 +2,18 @@
"ERROR": { "ERROR": {
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.", "UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
"Error, Please try again or contact the administrator": "Erreur. Veuillez réessayer ou contacter l'administrateur.", "Error, Please try again or contact the administrator": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
"File extension not allowed": "Extension de fichier non autorisée.",
"File size is greater than the allowed size": "La taille du fichier est supérieure à la limite autorisée.",
"Invalid credentials": "Identifiants invalides.", "Invalid credentials": "Identifiants invalides.",
"Network Error": "Erreur Réseau", "Invalid payload": "Données incorrectes.",
"Invalid token, Please log in again": "Jeton invalide. Merci de vous reconnecter.",
"No file part": "Pas de fichier fourni.",
"No selected file": "Pas de fichier sélectionné.",
"Network Error": "Erreur Réseau.",
"Provide a valid auth token": "Merci de fournir un jeton valide.",
"Password and password confirmation don't match": "Les mots de passe saisis sont différents.", "Password and password confirmation don't match": "Les mots de passe saisis sont différents.",
"Password: 8 characters required": "8 caractères minimum pour le mot de passe.", "Password: 8 characters required": "8 caractères minimum pour le mot de passe.",
"Signature expired, Please log in again": "Signature expirée. Merci de vous reconnecter.",
"Username: 3 to 12 characters required": "3 à 12 caractères requis pour le nom.", "Username: 3 to 12 characters required": "3 à 12 caractères requis pour le nom.",
"Valid email must be provided": "L'email fourni n'est pas valide." "Valid email must be provided": "L'email fourni n'est pas valide."
} }

View File

@ -10,16 +10,19 @@
"EDIT_WORKOUT": "Modifier la séance", "EDIT_WORKOUT": "Modifier la séance",
"ELEVATION": "altitude", "ELEVATION": "altitude",
"END": "fin", "END": "fin",
"GPX_FILE": ".gpx file", "GPX_FILE": "fichier .gpx",
"KM": "km", "KM": "km",
"LATEST_WORKOUTS": "Séances récentes", "LATEST_WORKOUTS": "Séances récentes",
"LOAD_MORE_WORKOUT": "Charger les séances suivantes", "LOAD_MORE_WORKOUT": "Charger les séances suivantes",
"MAX_ALTITUDE": "altitude max", "MAX_ALTITUDE": "altitude max",
"MAX_FILES": "fichiers max. ",
"MAX_SIZE": "taille max. ",
"MAX_SPEED": "vitesse max", "MAX_SPEED": "vitesse max",
"MIN_ALTITUDE": "altitude min", "MIN_ALTITUDE": "altitude min",
"NEXT_SEGMENT": "Segment suivant", "NEXT_SEGMENT": "Segment suivant",
"NEXT_WORKOUT": "Séance suivante", "NEXT_WORKOUT": "Séance suivante",
"NO_DATA_CLEANING": "données issues du fichier gpx, sans correction", "NO_DATA_CLEANING": "données issues du fichier gpx, sans correction",
"NO_FOLDER": "pas de répertoire",
"NO_MAP": "Pas de carte", "NO_MAP": "Pas de carte",
"NO_NEXT_SEGMENT": "Pas de segment suivant", "NO_NEXT_SEGMENT": "Pas de segment suivant",
"NO_NEXT_WORKOUT": "Pas de séance suivante", "NO_NEXT_WORKOUT": "Pas de séance suivante",
@ -66,5 +69,6 @@
"WORKOUT": "séance | séances", "WORKOUT": "séance | séances",
"WORKOUT_DATE": "date de la séance", "WORKOUT_DATE": "date de la séance",
"WORKOUT_DELETION_CONFIRMATION": "Etes-vous sûr de vouloir supprimer cette séance ?", "WORKOUT_DELETION_CONFIRMATION": "Etes-vous sûr de vouloir supprimer cette séance ?",
"ZIP_FILE": "ou une archive .zip contenant des fichiers .gpx" "ZIP_ARCHIVE": "archive .zip",
"ZIP_ARCHIVE_DESCRIPTION": "ou une archive .zip contenant des fichiers .gpx"
} }

View File

@ -32,7 +32,8 @@
--alert-background-color: #c8cdd3; --alert-background-color: #c8cdd3;
--alert-color: #3f3f3f; --alert-color: #3f3f3f;
--info-background-color: #e5e7ea;
--info-color: var(--app-color);
--error-background-color: #ffd2d2; --error-background-color: #ffd2d2;
--error-color: #db1924; --error-color: #db1924;

View File

@ -168,7 +168,7 @@ export const actions: ActionTree<IWorkoutsState, IRootState> &
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES) context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true) context.commit(WORKOUTS_STORE.MUTATIONS.SET_WORKOUT_LOADING, true)
if (!payload.file) { if (!payload.file) {
throw new Error('No gpx file provided') throw new Error('No file part')
} }
const form = new FormData() const form = new FormData()
form.append('file', payload.file) form.append('file', payload.file)

View File

@ -0,0 +1,14 @@
const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB']
export const getReadableFileSize = (
fileSize: number,
asText = true
): string | Record<string, string> => {
const i = Math.floor(Math.log(fileSize) / Math.log(1024))
if (!fileSize) {
return asText ? '0 bytes' : { size: '0', suffix: 'bytes' }
}
const size = (fileSize / Math.pow(1024, i)).toFixed(1)
const suffix = suffixes[i]
return asText ? `${size}${suffix}` : { size, suffix }
}

View File

@ -17,8 +17,7 @@ export const getApiUrl = (): string => {
// TODO: update api error messages to remove these workarounds // TODO: update api error messages to remove these workarounds
const removeLastEndOfLine = (text: string): string => text.replace(/\n$/gm, '') const removeLastEndOfLine = (text: string): string => text.replace(/\n$/gm, '')
const removeLastDot = (text: string): string => text.replace(/\.$/gm, '') const removeLastDot = (text: string): string => text.replace(/\.$/gm, '')
const removeErrorDot = (text: string): string => const replaceInternalDots = (text: string): string => text.replace(/\./gm, ',')
text.replace(/^Error\./gm, 'Error,')
export const handleError = ( export const handleError = (
context: context:
@ -33,14 +32,16 @@ export const handleError = (
let errorMessages = !error let errorMessages = !error
? msg ? msg
: error.response : error.response
? error.response.data.message ? error.response.status === 413
? 'File size is greater than the allowed size'
: error.response.data.message
? error.response.data.message ? error.response.data.message
: msg : msg
: error.message : error.message
? error.message ? error.message
: msg : msg
errorMessages = removeLastEndOfLine(errorMessages) errorMessages = removeLastEndOfLine(errorMessages)
errorMessages = removeErrorDot(errorMessages) errorMessages = replaceInternalDots(errorMessages)
context.commit( context.commit(
ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES, ROOT_STORE.MUTATIONS.SET_ERROR_MESSAGES,
errorMessages.includes('\n') errorMessages.includes('\n')

View File

@ -0,0 +1,61 @@
import { assert } from 'chai'
import { getReadableFileSize } from '@/utils/files'
describe('getReadableFileSize (as text)', () => {
const testsParams = [
{
description: 'returns 0 bytes if provided file size is 0',
inputFileSize: 0,
expectedReadableFileSize: '0 bytes',
},
{
description: 'returns 1.0KB if provided file size is 1024',
inputFileSize: 1024,
expectedReadableFileSize: '1.0KB',
},
{
description: 'returns 43.5MB if provided file size is 45654654',
inputFileSize: 45654654,
expectedReadableFileSize: '43.5MB',
},
]
testsParams.map((testParams) => {
it(testParams.description, () => {
assert.equal(
getReadableFileSize(testParams.inputFileSize, true),
testParams.expectedReadableFileSize
)
})
})
})
describe('getReadableFileSize (as object)', () => {
const testsParams = [
{
description: 'returns 0 bytes if provided file size is 0',
inputFileSize: 0,
expectedReadableFileSize: { size: '0', suffix: 'bytes' },
},
{
description: 'returns 1.0KB if provided file size is 1024',
inputFileSize: 1024,
expectedReadableFileSize: { size: '1.0', suffix: 'KB' },
},
{
description: 'returns 43.5MB if provided file size is 45654654',
inputFileSize: 45654654,
expectedReadableFileSize: { size: '43.5', suffix: 'MB' },
},
]
testsParams.map((testParams) => {
it(testParams.description, () => {
assert.deepEqual(
getReadableFileSize(testParams.inputFileSize, false),
testParams.expectedReadableFileSize
)
})
})
})