Client - add application administration
This commit is contained in:
parent
04cf43cfd2
commit
2ba931dcf7
@ -36,7 +36,7 @@
|
|||||||
import NavBar from '@/components/NavBar.vue'
|
import NavBar from '@/components/NavBar.vue'
|
||||||
import NoConfig from '@/components/NoConfig.vue'
|
import NoConfig from '@/components/NoConfig.vue'
|
||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { IAppConfig } from '@/types/application'
|
import { TAppConfig } from '@/types/application'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -49,7 +49,7 @@
|
|||||||
setup() {
|
setup() {
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const appConfig: ComputedRef<IAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
const appLoading: ComputedRef<boolean> = computed(
|
const appLoading: ComputedRef<boolean> = computed(
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div id="admin-app" class="admin-card">
|
||||||
|
<Card>
|
||||||
|
<template #title>{{ $t('admin.APP_CONFIG.TITLE') }}</template>
|
||||||
|
<template #content>
|
||||||
|
<form class="admin-form" @submit.prevent="onSubmit">
|
||||||
|
<label for="max_users">
|
||||||
|
{{ $t('admin.APP_CONFIG.MAX_USERS_LABEL') }}:
|
||||||
|
<input
|
||||||
|
id="max_users"
|
||||||
|
name="max_users"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="appData.max_users"
|
||||||
|
:disabled="!edition"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label for="max_single_file_size">
|
||||||
|
{{ $t('admin.APP_CONFIG.SINGLE_UPLOAD_MAX_SIZE_LABEL') }}:
|
||||||
|
<input
|
||||||
|
id="max_single_file_size"
|
||||||
|
name="max_single_file_size"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
v-model="appData.max_single_file_size"
|
||||||
|
:disabled="!edition"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label for="max_zip_file_size">
|
||||||
|
{{ $t('admin.APP_CONFIG.ZIP_UPLOAD_MAX_SIZE_LABEL') }}:
|
||||||
|
<input
|
||||||
|
id="max_zip_file_size"
|
||||||
|
name="max_zip_file_size"
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
v-model="appData.max_zip_file_size"
|
||||||
|
:disabled="!edition"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label for="gpx_limit_import">
|
||||||
|
{{ $t('admin.APP_CONFIG.MAX_FILES_IN_ZIP_LABEL') }}:
|
||||||
|
<input
|
||||||
|
id="gpx_limit_import"
|
||||||
|
name="gpx_limit_import"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model="appData.gpx_limit_import"
|
||||||
|
:disabled="!edition"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
|
||||||
|
<div class="form-buttons" v-if="edition">
|
||||||
|
<button class="confirm" type="submit">
|
||||||
|
{{ $t('buttons.SUBMIT') }}
|
||||||
|
</button>
|
||||||
|
<button class="cancel" @click.prevent="onCancel">
|
||||||
|
{{ $t('buttons.CANCEL') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-buttons" v-else>
|
||||||
|
<button
|
||||||
|
class="confirm"
|
||||||
|
@click.prevent="$router.push('/admin/application/edit')"
|
||||||
|
>
|
||||||
|
{{ $t('buttons.EDIT') }}
|
||||||
|
</button>
|
||||||
|
<button class="cancel" @click.prevent="$router.push('/admin')">
|
||||||
|
{{ $t('admin.BACK_TO_ADMIN') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ComputedRef,
|
||||||
|
PropType,
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
reactive,
|
||||||
|
onBeforeMount,
|
||||||
|
} from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
|
import { TAppConfig, TAppConfigForm } from '@/types/application'
|
||||||
|
import { useStore } from '@/use/useStore'
|
||||||
|
import { getFileSizeInMB } from '@/utils/files'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'AdminApplication',
|
||||||
|
props: {
|
||||||
|
appConfig: {
|
||||||
|
type: Object as PropType<TAppConfig>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
edition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const appData: TAppConfigForm = reactive({
|
||||||
|
max_users: 0,
|
||||||
|
max_single_file_size: 0,
|
||||||
|
max_zip_file_size: 0,
|
||||||
|
gpx_limit_import: 0,
|
||||||
|
})
|
||||||
|
const errorMessages: ComputedRef<string | string[] | null> = computed(
|
||||||
|
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (props.appConfig) {
|
||||||
|
updateForm(props.appConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateForm(appConfig: TAppConfig) {
|
||||||
|
Object.keys(appData).map((key) => {
|
||||||
|
;['max_single_file_size', 'max_zip_file_size'].includes(key)
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
(appData[key] = getFileSizeInMB(appConfig[key]))
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
(appData[key] = appConfig[key])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
updateForm(props.appConfig)
|
||||||
|
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||||
|
router.push('/admin/application')
|
||||||
|
}
|
||||||
|
function onSubmit() {
|
||||||
|
const formData: TAppConfigForm = Object.assign({}, appData)
|
||||||
|
formData.max_single_file_size *= 1048576
|
||||||
|
formData.max_zip_file_size *= 1048576
|
||||||
|
store.dispatch(ROOT_STORE.ACTIONS.UPDATE_APPLICATION_CONFIG, formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { appData, errorMessages, onCancel, onSubmit }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
@ -6,7 +6,11 @@
|
|||||||
<AppStatsCards :app-statistics="appStatistics" />
|
<AppStatsCards :app-statistics="appStatistics" />
|
||||||
<div class="admin-menu description-list">
|
<div class="admin-menu description-list">
|
||||||
<dl>
|
<dl>
|
||||||
<dt>{{ $t('admin.APPLICATION') }}</dt>
|
<dt>
|
||||||
|
<router-link to="/admin/application">
|
||||||
|
{{ $t('admin.APPLICATION') }}
|
||||||
|
</router-link>
|
||||||
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ $t('admin.UPDATE_APPLICATION_DESCRIPTION') }}
|
{{ $t('admin.UPDATE_APPLICATION_DESCRIPTION') }}
|
||||||
</dd>
|
</dd>
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
import UserPicture from '@/components/User/UserPicture.vue'
|
import UserPicture from '@/components/User/UserPicture.vue'
|
||||||
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
||||||
import { IAppConfig } from '@/types/application'
|
import { TAppConfig } from '@/types/application'
|
||||||
import { IAuthUserProfile } from '@/types/user'
|
import { IAuthUserProfile } from '@/types/user'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
import { getReadableFileSize } from '@/utils/files'
|
import { getReadableFileSize } from '@/utils/files'
|
||||||
@ -60,7 +60,7 @@
|
|||||||
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]
|
||||||
)
|
)
|
||||||
const appConfig: ComputedRef<IAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
const fileSizeLimit = appConfig.value.max_single_file_size
|
const fileSizeLimit = appConfig.value.max_single_file_size
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
||||||
import { IAppConfig } from '@/types/application'
|
import { TAppConfig } from '@/types/application'
|
||||||
import { ILoginRegisterFormData } from '@/types/user'
|
import { ILoginRegisterFormData } from '@/types/user'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
|
|
||||||
@ -113,7 +113,7 @@
|
|||||||
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]
|
||||||
)
|
)
|
||||||
const appConfig: ComputedRef<IAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
const registration_disabled: ComputedRef<boolean> = computed(
|
const registration_disabled: ComputedRef<boolean> = computed(
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
|
import { ComputedRef, PropType, computed, defineComponent, ref } from 'vue'
|
||||||
|
|
||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { IAppConfig } from '@/types/application'
|
import { TAppConfig } from '@/types/application'
|
||||||
import { GeoJSONData } from '@/types/geojson'
|
import { GeoJSONData } from '@/types/geojson'
|
||||||
import { IWorkoutData, TCoordinates } from '@/types/workouts'
|
import { IWorkoutData, TCoordinates } from '@/types/workouts'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
@ -103,7 +103,7 @@
|
|||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
const appConfig: ComputedRef<IAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
const center = computed(() => getCenter(bounds))
|
const center = computed(() => getCenter(bounds))
|
||||||
|
@ -221,7 +221,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
|
import { ROOT_STORE, WORKOUTS_STORE } from '@/store/constants'
|
||||||
import { IAppConfig } from '@/types/application'
|
import { TAppConfig } 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'
|
||||||
@ -268,7 +268,7 @@
|
|||||||
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
const translatedSports: ComputedRef<ISport[]> = computed(() =>
|
||||||
translateSports(props.sports, t)
|
translateSports(props.sports, t)
|
||||||
)
|
)
|
||||||
const appConfig: ComputedRef<IAppConfig> = computed(
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
)
|
)
|
||||||
const fileSizeLimit = appConfig.value.max_single_file_size
|
const fileSizeLimit = appConfig.value.max_single_file_size
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
"ADMIN": "Admin",
|
"ADMIN": "Admin",
|
||||||
"ADMINISTRATION": "Administration",
|
"ADMINISTRATION": "Administration",
|
||||||
"APPLICATION": "Application",
|
"APPLICATION": "Application",
|
||||||
|
"APP_CONFIG": {
|
||||||
|
"MAX_USERS_LABEL": "Max. number of active users",
|
||||||
|
"MAX_FILES_IN_ZIP_LABEL": "Max. files of zip archive",
|
||||||
|
"SINGLE_UPLOAD_MAX_SIZE_LABEL": "Max. size of uploaded files (in Mb)",
|
||||||
|
"TITLE": "Application configuration",
|
||||||
|
"ZIP_UPLOAD_MAX_SIZE_LABEL": "Max. size of zip archive (in Mb)"
|
||||||
|
},
|
||||||
|
"BACK_TO_ADMIN": "Back to admin",
|
||||||
"ENABLE_DISABLE_SPORTS": "Enable/disable sports.",
|
"ENABLE_DISABLE_SPORTS": "Enable/disable sports.",
|
||||||
"REGISTRATION_DISABLED": "Registration is currently disabled.",
|
"REGISTRATION_DISABLED": "Registration is currently disabled.",
|
||||||
"REGISTRATION_ENABLED": "Registration is currently enabled.",
|
"REGISTRATION_ENABLED": "Registration is currently enabled.",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"CANCEL": "Cancel",
|
"CANCEL": "Cancel",
|
||||||
"DELETE_MY_ACCOUNT": "Delete my account",
|
"DELETE_MY_ACCOUNT": "Delete my account",
|
||||||
|
"EDIT": "Edit",
|
||||||
"FILTER": "Filter",
|
"FILTER": "Filter",
|
||||||
"LOGIN": "Log in",
|
"LOGIN": "Log in",
|
||||||
"NO": "No",
|
"NO": "No",
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
"ADMIN": "Admin",
|
"ADMIN": "Admin",
|
||||||
"ADMINISTRATION": "Administration",
|
"ADMINISTRATION": "Administration",
|
||||||
"APPLICATION": "Application",
|
"APPLICATION": "Application",
|
||||||
|
"APP_CONFIG": {
|
||||||
|
"MAX_USERS_LABEL": "Nombre maximum d'utilisateurs actifs ",
|
||||||
|
"MAX_FILES_IN_ZIP_LABEL": "Taille max. des archives zip (en Mo) ",
|
||||||
|
"SINGLE_UPLOAD_MAX_SIZE_LABEL": "Taille max. des fichiers (en Mo) ",
|
||||||
|
"TITLE": "Configuration de l'application",
|
||||||
|
"ZIP_UPLOAD_MAX_SIZE_LABEL": "Nombre max. de fichiers dans une archive zip "
|
||||||
|
},
|
||||||
|
"BACK_TO_ADMIN": "Revenir à l'admin",
|
||||||
"ENABLE_DISABLE_SPORTS": "Activer/désactiver des sports.",
|
"ENABLE_DISABLE_SPORTS": "Activer/désactiver des sports.",
|
||||||
"REGISTRATION_DISABLED": "Les inscriptions sont actuellement désactivées.",
|
"REGISTRATION_DISABLED": "Les inscriptions sont actuellement désactivées.",
|
||||||
"REGISTRATION_ENABLED": "Les inscriptions sont actuellement activées.",
|
"REGISTRATION_ENABLED": "Les inscriptions sont actuellement activées.",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"CANCEL": "Annuler",
|
"CANCEL": "Annuler",
|
||||||
"DELETE_MY_ACCOUNT": "Supprimer mon compte",
|
"DELETE_MY_ACCOUNT": "Supprimer mon compte",
|
||||||
|
"EDIT": "Modifier",
|
||||||
"FILTER": "Filtrer",
|
"FILTER": "Filtrer",
|
||||||
"LOGIN": "Se connecter",
|
"LOGIN": "Se connecter",
|
||||||
"NO": "Non",
|
"NO": "Non",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import AdminApplication from '@/components/Administration/AdminApplication.vue'
|
||||||
|
import AdminMenu from '@/components/Administration/AdminMenu.vue'
|
||||||
import Profile from '@/components/User/ProfileDisplay/index.vue'
|
import Profile from '@/components/User/ProfileDisplay/index.vue'
|
||||||
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
|
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
|
||||||
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
|
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
|
||||||
@ -167,6 +169,24 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'Administration',
|
name: 'Administration',
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: 'admin' */ '@/views/AdminView.vue'),
|
import(/* webpackChunkName: 'admin' */ '@/views/AdminView.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'AdministrationMenu',
|
||||||
|
component: AdminMenu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'application',
|
||||||
|
name: 'ApplicationAdministration',
|
||||||
|
component: AdminApplication,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'application/edit',
|
||||||
|
name: 'ApplicationAdministrationEdition',
|
||||||
|
component: AdminApplication,
|
||||||
|
props: { edition: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { ActionContext, ActionTree } from 'vuex'
|
import { ActionContext, ActionTree } from 'vuex'
|
||||||
|
|
||||||
import authApi from '@/api/authApi'
|
import authApi from '@/api/authApi'
|
||||||
|
import router from '@/router'
|
||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { IRootActions, IRootState } from '@/store/modules/root/types'
|
import { IRootActions, IRootState } from '@/store/modules/root/types'
|
||||||
|
import { TAppConfigForm } from '@/types/application'
|
||||||
import { handleError } from '@/utils'
|
import { handleError } from '@/utils'
|
||||||
|
|
||||||
export const actions: ActionTree<IRootState, IRootState> & IRootActions = {
|
export const actions: ActionTree<IRootState, IRootState> & IRootActions = {
|
||||||
@ -46,4 +48,24 @@ export const actions: ActionTree<IRootState, IRootState> & IRootActions = {
|
|||||||
})
|
})
|
||||||
.catch((error) => handleError(context, error))
|
.catch((error) => handleError(context, error))
|
||||||
},
|
},
|
||||||
|
[ROOT_STORE.ACTIONS.UPDATE_APPLICATION_CONFIG](
|
||||||
|
context: ActionContext<IRootState, IRootState>,
|
||||||
|
payload: TAppConfigForm
|
||||||
|
): void {
|
||||||
|
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
|
||||||
|
authApi
|
||||||
|
.patch('config', payload)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.status === 'success') {
|
||||||
|
context.commit(
|
||||||
|
ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_CONFIG,
|
||||||
|
res.data.data
|
||||||
|
)
|
||||||
|
router.push('/admin/application')
|
||||||
|
} else {
|
||||||
|
handleError(context, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => handleError(context, error))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export enum RootActions {
|
export enum RootActions {
|
||||||
GET_APPLICATION_CONFIG = 'GET_APPLICATION_CONFIG',
|
GET_APPLICATION_CONFIG = 'GET_APPLICATION_CONFIG',
|
||||||
GET_APPLICATION_STATS = 'GET_APPLICATION_STATS',
|
GET_APPLICATION_STATS = 'GET_APPLICATION_STATS',
|
||||||
|
UPDATE_APPLICATION_CONFIG = 'UPDATE_APPLICATION_CONFIG',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RootGetters {
|
export enum RootGetters {
|
||||||
|
@ -2,7 +2,7 @@ import { MutationTree } from 'vuex'
|
|||||||
|
|
||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { IRootState, TRootMutations } from '@/store/modules/root/types'
|
import { IRootState, TRootMutations } from '@/store/modules/root/types'
|
||||||
import { IAppConfig, IAppStatistics } from '@/types/application'
|
import { TAppConfig, IAppStatistics } from '@/types/application'
|
||||||
import { localeFromLanguage } from '@/utils/locales'
|
import { localeFromLanguage } from '@/utils/locales'
|
||||||
|
|
||||||
export const mutations: MutationTree<IRootState> & TRootMutations = {
|
export const mutations: MutationTree<IRootState> & TRootMutations = {
|
||||||
@ -17,7 +17,7 @@ export const mutations: MutationTree<IRootState> & TRootMutations = {
|
|||||||
},
|
},
|
||||||
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_CONFIG](
|
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_CONFIG](
|
||||||
state: IRootState,
|
state: IRootState,
|
||||||
config: IAppConfig
|
config: TAppConfig
|
||||||
) {
|
) {
|
||||||
state.application.config = config
|
state.application.config = config
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,12 @@ import {
|
|||||||
} from 'vuex'
|
} from 'vuex'
|
||||||
|
|
||||||
import { ROOT_STORE } from '@/store/constants'
|
import { ROOT_STORE } from '@/store/constants'
|
||||||
import { IAppConfig, IApplication, IAppStatistics } from '@/types/application'
|
import {
|
||||||
|
TAppConfig,
|
||||||
|
IApplication,
|
||||||
|
IAppStatistics,
|
||||||
|
TAppConfigForm,
|
||||||
|
} from '@/types/application'
|
||||||
|
|
||||||
export interface IRootState {
|
export interface IRootState {
|
||||||
root: boolean
|
root: boolean
|
||||||
@ -25,10 +30,14 @@ export interface IRootActions {
|
|||||||
[ROOT_STORE.ACTIONS.GET_APPLICATION_STATS](
|
[ROOT_STORE.ACTIONS.GET_APPLICATION_STATS](
|
||||||
context: ActionContext<IRootState, IRootState>
|
context: ActionContext<IRootState, IRootState>
|
||||||
): void
|
): void
|
||||||
|
[ROOT_STORE.ACTIONS.UPDATE_APPLICATION_CONFIG](
|
||||||
|
context: ActionContext<IRootState, IRootState>,
|
||||||
|
payload: TAppConfigForm
|
||||||
|
): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRootGetters {
|
export interface IRootGetters {
|
||||||
[ROOT_STORE.GETTERS.APP_CONFIG](state: IRootState): IAppConfig
|
[ROOT_STORE.GETTERS.APP_CONFIG](state: IRootState): TAppConfig
|
||||||
|
|
||||||
[ROOT_STORE.GETTERS.APP_LOADING](state: IRootState): boolean
|
[ROOT_STORE.GETTERS.APP_LOADING](state: IRootState): boolean
|
||||||
|
|
||||||
@ -51,7 +60,7 @@ export type TRootMutations<S = IRootState> = {
|
|||||||
): void
|
): void
|
||||||
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_CONFIG](
|
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_CONFIG](
|
||||||
state: S,
|
state: S,
|
||||||
config: IAppConfig
|
config: TAppConfig
|
||||||
): void
|
): void
|
||||||
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_LOADING](
|
[ROOT_STORE.MUTATIONS.UPDATE_APPLICATION_LOADING](
|
||||||
state: S,
|
state: S,
|
||||||
|
@ -5,17 +5,26 @@ export interface IAppStatistics {
|
|||||||
workouts: number
|
workouts: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAppConfig {
|
export type TAppConfig = {
|
||||||
|
[key: string]: number | boolean | string
|
||||||
federation_enabled: boolean
|
federation_enabled: boolean
|
||||||
gpx_limit_import: number | null
|
gpx_limit_import: number
|
||||||
is_registration_enabled: boolean
|
is_registration_enabled: boolean
|
||||||
map_attribution: string
|
map_attribution: string
|
||||||
max_single_file_size: number | null
|
max_single_file_size: number
|
||||||
max_users: number | null
|
max_users: number
|
||||||
max_zip_file_size: number | null
|
max_zip_file_size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApplication {
|
export interface IApplication {
|
||||||
statistics: IAppStatistics
|
statistics: IAppStatistics
|
||||||
config: IAppConfig
|
config: TAppConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TAppConfigForm = {
|
||||||
|
[key: string]: number
|
||||||
|
gpx_limit_import: number
|
||||||
|
max_single_file_size: number
|
||||||
|
max_users: number
|
||||||
|
max_zip_file_size: number
|
||||||
}
|
}
|
||||||
|
@ -12,3 +12,8 @@ export const getReadableFileSize = (
|
|||||||
const suffix = suffixes[i]
|
const suffix = suffixes[i]
|
||||||
return asText ? `${size}${suffix}` : { size, suffix }
|
return asText ? `${size}${suffix}` : { size, suffix }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFileSizeInMB = (fileSize: number): number => {
|
||||||
|
const value = fileSize / 1048576
|
||||||
|
return (!fileSize && 0) || +value.toFixed(2)
|
||||||
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="admin">
|
<div id="admin">
|
||||||
<div class="container" v-if="!userLoading">
|
<div class="container" v-if="!userLoading">
|
||||||
<AdministrationMenu
|
<router-view
|
||||||
v-if="isAuthUserAmin"
|
v-if="isAuthUserAmin"
|
||||||
|
:appConfig="appConfig"
|
||||||
:appStatistics="appStatistics"
|
:appStatistics="appStatistics"
|
||||||
/>
|
/>
|
||||||
<NotFound v-else />
|
<NotFound v-else />
|
||||||
@ -13,16 +14,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, ComputedRef, defineComponent, onBeforeMount } from 'vue'
|
import { computed, ComputedRef, defineComponent, onBeforeMount } from 'vue'
|
||||||
|
|
||||||
import AdministrationMenu from '@/components/Administration/AdminMenu.vue'
|
|
||||||
import NotFound from '@/components/Common/NotFound.vue'
|
import NotFound from '@/components/Common/NotFound.vue'
|
||||||
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
import { ROOT_STORE, USER_STORE } from '@/store/constants'
|
||||||
import { IAppStatistics } from '@/types/application'
|
import { TAppConfig, IAppStatistics } from '@/types/application'
|
||||||
import { useStore } from '@/use/useStore'
|
import { useStore } from '@/use/useStore'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
components: {
|
components: {
|
||||||
AdministrationMenu,
|
|
||||||
NotFound,
|
NotFound,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
@ -35,6 +34,9 @@
|
|||||||
const appLoading: ComputedRef<boolean> = computed(
|
const appLoading: ComputedRef<boolean> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_LOADING]
|
() => store.getters[ROOT_STORE.GETTERS.APP_LOADING]
|
||||||
)
|
)
|
||||||
|
const appConfig: ComputedRef<TAppConfig> = computed(
|
||||||
|
() => store.getters[ROOT_STORE.GETTERS.APP_CONFIG]
|
||||||
|
)
|
||||||
const appStatistics: ComputedRef<IAppStatistics> = computed(
|
const appStatistics: ComputedRef<IAppStatistics> = computed(
|
||||||
() => store.getters[ROOT_STORE.GETTERS.APP_STATS]
|
() => store.getters[ROOT_STORE.GETTERS.APP_STATS]
|
||||||
)
|
)
|
||||||
@ -45,11 +47,62 @@
|
|||||||
() => store.getters[USER_STORE.GETTERS.USER_LOADING]
|
() => store.getters[USER_STORE.GETTERS.USER_LOADING]
|
||||||
)
|
)
|
||||||
|
|
||||||
return { appLoading, appStatistics, isAuthUserAmin, userLoading }
|
return {
|
||||||
|
appConfig,
|
||||||
|
appLoading,
|
||||||
|
appStatistics,
|
||||||
|
isAuthUserAmin,
|
||||||
|
userLoading,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/scss/base.scss';
|
@import '~@/scss/base.scss';
|
||||||
|
|
||||||
|
#admin {
|
||||||
|
.admin-card {
|
||||||
|
width: 100%;
|
||||||
|
::v-deep(.card) {
|
||||||
|
.admin-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: $default-margin 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 50%;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-right: $default-margin * 5;
|
||||||
|
@media screen and (max-width: $medium-limit) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: $small-limit) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
background-color: white;
|
||||||
|
border-color: white;
|
||||||
|
color: var(--app-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: $default-padding;
|
||||||
|
margin-bottom: $default-margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { assert } from 'chai'
|
import { assert } from 'chai'
|
||||||
|
|
||||||
import { getReadableFileSize } from '@/utils/files'
|
import { getFileSizeInMB, getReadableFileSize } from '@/utils/files'
|
||||||
|
|
||||||
describe('getReadableFileSize (as text)', () => {
|
describe('getReadableFileSize (as text)', () => {
|
||||||
const testsParams = [
|
const testsParams = [
|
||||||
@ -59,3 +59,32 @@ describe('getReadableFileSize (as object)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getFileSizeInMB', () => {
|
||||||
|
const testsParams = [
|
||||||
|
{
|
||||||
|
description: 'returns 0 if provided file size is 0',
|
||||||
|
inputFileSize: 0,
|
||||||
|
expectedFileSize: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns 1 (MB) if provided file size is 1048576',
|
||||||
|
inputFileSize: 1048576,
|
||||||
|
expectedFileSize: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'returns 2.53 (MB) if provided file size is 2652897',
|
||||||
|
inputFileSize: 2652897,
|
||||||
|
expectedFileSize: 2.53,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
testsParams.map((testParams) => {
|
||||||
|
it(testParams.description, () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
getFileSizeInMB(testParams.inputFileSize),
|
||||||
|
testParams.expectedFileSize
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user