Merge remote-tracking branch 'upstream/develop' into feature/theming2

* upstream/develop:
  Fix color fallback order
  Use console.warn instead of console.log
  Get rid of mutation_types file, use inline approach. Minor fixes
  Add fallback color rule.
  Change english validation error messages
  Clean up the code
  Validate name presence on client-side as well
  Better styling for client-side validation. Add I18n for validation errors.
  Fix broken ToS link. Fix linter errors
  Add client validation for registration form
  Use Array.reduce instead of lodash.reduce
  Humanize validation errors returned on registration
  Added user option to hide instance-specific panel, rearranged config screen to better categorize it / adjustments to language selector
  fix
This commit is contained in:
Henry Jameson 2018-12-11 01:01:16 +03:00
commit 3452864260
14 changed files with 271 additions and 1056 deletions

View file

@ -31,6 +31,7 @@
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2",
"vuelidate": "^0.7.4",
"vuex": "^3.0.1",
"whatwg-fetch": "^2.0.3"
},

View file

@ -58,6 +58,7 @@ const afterStoreSetup = ({store, i18n}) => {
var loginMethod = (config.loginMethod)
var scopeCopy = (config.scopeCopy)
var subjectLineBehavior = (config.subjectLineBehavior)
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
store.dispatch('setInstanceOption', { name: 'background', value: background })
@ -75,6 +76,7 @@ const afterStoreSetup = ({store, i18n}) => {
store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod })
store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy })
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
if (chatDisabled) {
store.dispatch('disableChat')
}

View file

@ -2,6 +2,9 @@ const InstanceSpecificPanel = {
computed: {
instanceSpecificPanelContent () {
return this.$store.state.instance.instanceSpecificPanelContent
},
show () {
return !this.$store.state.config.hideISP
}
}
}

View file

@ -1,5 +1,5 @@
<template>
<div class="instance-specific-panel">
<div v-if="show" class="instance-specific-panel">
<div class="panel panel-default">
<div class="panel-body">
<div v-html="instanceSpecificPanelContent">

View file

@ -1,5 +1,8 @@
<template>
<div>
<label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }}
</label>
<label for="interface-language-switcher" class='select'>
<select id="interface-language-switcher" v-model="language">
<option v-for="(langCode, i) in languageCodes" :value="langCode">

View file

@ -1,57 +1,61 @@
import oauthApi from '../../services/new_api/oauth.js'
import { validationMixin } from 'vuelidate'
import { required, sameAs } from 'vuelidate/lib/validators'
import { mapActions, mapState } from 'vuex'
const registration = {
mixins: [validationMixin],
data: () => ({
user: {},
error: false,
registering: false
}),
created () {
if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all')
user: {
email: '',
fullname: '',
username: '',
password: '',
confirm: ''
}
// Seems like this doesn't work at first page open for some reason
if (this.$store.state.instance.registrationOpen && this.token) {
this.$router.push('/registration')
}),
validations: {
user: {
email: { required },
username: { required },
fullname: { required },
password: { required },
confirm: {
required,
sameAsPassword: sameAs('password')
}
}
},
created () {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push('/main/all')
}
},
computed: {
termsofservice () { return this.$store.state.instance.tos },
token () { return this.$route.params.token }
token () { return this.$route.params.token },
...mapState({
registrationOpen: (state) => state.instance.registrationOpen,
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos
})
},
methods: {
submit () {
this.registering = true
...mapActions(['signUp']),
async submit () {
this.user.nickname = this.user.username
this.user.token = this.token
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
const data = {
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server
}
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$v.$touch()
if (!this.$v.$invalid) {
try {
await this.signUp(this.user)
this.$router.push('/main/friends')
})
})
} else {
this.registering = false
response.json().then((data) => {
this.error = data.error
})
} catch (error) {
console.warn('Registration failed: ' + error)
}
}
)
}
}
}

View file

@ -7,50 +7,90 @@
<form v-on:submit.prevent='submit(user)' class='registration-form'>
<div class='container'>
<div class='text-fields'>
<div class='form-group'>
<label for='username'>{{$t('login.username')}}</label>
<input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
<div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
<label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
</div>
<div class='form-group'>
<label for='fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
<div class="form-error" v-if="$v.user.username.$dirty">
<ul>
<li v-if="!$v.user.username.required">
<span>{{$t('registration.validations.username_required')}}</span>
</li>
</ul>
</div>
<div class='form-group'>
<label for='email'>{{$t('registration.email')}}</label>
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
</div>
<div class='form-group'>
<label for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
<div class="form-error" v-if="$v.user.fullname.$dirty">
<ul>
<li v-if="!$v.user.fullname.required">
<span>{{$t('registration.validations.fullname_required')}}</span>
</li>
</ul>
</div>
<div class='form-group'>
<label for='password'>{{$t('login.password')}}</label>
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
<div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
<label class='form--label' for='email'>{{$t('registration.email')}}</label>
<input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
</div>
<div class='form-group'>
<label for='password_confirmation'>{{$t('registration.password_confirm')}}</label>
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
<div class="form-error" v-if="$v.user.email.$dirty">
<ul>
<li v-if="!$v.user.email.required">
<span>{{$t('registration.validations.email_required')}}</span>
</li>
</ul>
</div>
<!--
<div class='form-group'>
<label for='captcha'>Captcha</label>
<img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
</div>
-->
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
<label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
<input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
</div>
<div class="form-error" v-if="$v.user.password.$dirty">
<ul>
<li v-if="!$v.user.password.required">
<span>{{$t('registration.validations.password_required')}}</span>
</li>
</ul>
</div>
<div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
<label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
<input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
</div>
<div class="form-error" v-if="$v.user.confirm.$dirty">
<ul>
<li v-if="!$v.user.confirm.required">
<span>{{$t('registration.validations.password_confirmation_required')}}</span>
</li>
<li v-if="!$v.user.confirm.sameAsPassword">
<span>{{$t('registration.validations.password_confirmation_match')}}</span>
</li>
</ul>
</div>
<div class='form-group' v-if='token' >
<label for='token'>{{$t('registration.token')}}</label>
<input disabled='true' v-model='token' class='form-control' id='token' type='text'>
</div>
<div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
<button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
</div>
</div>
<div class='terms-of-service' v-html="termsofservice">
<div class='terms-of-service' v-html="termsOfService">
</div>
</div>
<div v-if="error" class='form-group'>
<div class='alert error'>{{error}}</div>
<div v-if="serverValidationErrors.length" class='form-group'>
<div class='alert error'>
<span v-for="error in serverValidationErrors">{{error}}</span>
</div>
</div>
</form>
</div>
@ -60,6 +100,7 @@
<script src="./registration.js"></script>
<style lang="scss">
@import '../../_variables.scss';
$validations-cRed: #f04124;
.registration-form {
display: flex;
@ -89,6 +130,55 @@
flex-direction: column;
padding: 0.3em 0.0em 0.3em;
line-height:24px;
margin-bottom: 1em;
}
@keyframes shakeError {
0% {
transform: translateX(0); }
15% {
transform: translateX(0.375rem); }
30% {
transform: translateX(-0.375rem); }
45% {
transform: translateX(0.375rem); }
60% {
transform: translateX(-0.375rem); }
75% {
transform: translateX(0.375rem); }
90% {
transform: translateX(-0.375rem); }
100% {
transform: translateX(0); } }
.form-group--error {
animation-name: shakeError;
animation-duration: .6s;
animation-timing-function: ease-in-out;
}
.form-group--error .form--label {
color: $validations-cRed;
color: var(--cRed, $validations-cRed);
}
.form-error {
margin-top: -0.7em;
text-align: left;
span {
font-size: 12px;
}
}
.form-error ul {
list-style: none;
padding: 0 0 0 5px;
margin-top: 0;
li::before {
content: "• ";
}
}
form textarea {
@ -102,8 +192,6 @@
}
.btn {
//align-self: flex-start;
//width: 10em;
margin-top: 0.6em;
height: 28px;
}

View file

@ -13,6 +13,7 @@ const settings = {
hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
hideNsfwLocal: user.hideNsfw,
hideISPLocal: user.hideISP,
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
? instance.hidePostStats
: user.hidePostStats,
@ -83,6 +84,9 @@ const settings = {
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
hideISPLocal (value) {
this.$store.dispatch('setOption', { name: 'hideISP', value })
},
'notificationVisibilityLocal.likes' (value) {
this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
},

View file

@ -22,8 +22,16 @@
<tab-switcher>
<div :label="$t('settings.general')" >
<div class="setting-item">
<h2>{{ $t('settings.interfaceLanguage') }}</h2>
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li>
<interface-language-switcher />
</li>
<li>
<input type="checkbox" id="hideISP" v-model="hideISPLocal">
<label for="hideISP">{{$t('settings.hide_isp')}}</label>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{$t('nav.timeline')}}</h2>

View file

@ -72,7 +72,15 @@
"fullname": "Display name",
"password_confirm": "Password confirmation",
"registration": "Registration",
"token": "Invite token"
"token": "Invite token",
"validations": {
"username_required": "cannot be left blank",
"fullname_required": "cannot be left blank",
"email_required": "cannot be left blank",
"password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password"
}
},
"settings": {
"attachmentRadius": "Attachments",
@ -116,6 +124,7 @@
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
"hide_isp": "Hide instance-specific panel",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"import_followers_from_a_csv_file": "Import follows from a csv file",
@ -124,6 +133,7 @@
"checkboxRadius": "Checkboxes",
"instance_default": "(default: {value})",
"instance_default_simple" : "(default)",
"interface": "Interface",
"interfaceLanguage": "Interface language",
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
"limited_availability": "Unavailable in your browser",

View file

@ -55,7 +55,15 @@
"fullname": "Отображаемое имя",
"password_confirm": "Подтверждение пароля",
"registration": "Регистрация",
"token": "Код приглашения"
"token": "Код приглашения",
"validations": {
"username_required": "не должно быть пустым",
"fullname_required": "не должно быть пустым",
"email_required": "не должен быть пустым",
"password_required": "не должен быть пустым",
"password_confirmation_required": "не должно быть пустым",
"password_confirmation_match": "должно совпадать с паролем"
}
},
"settings": {
"attachmentRadius": "Прикреплённые файлы",
@ -97,10 +105,12 @@
"general": "Общие",
"hide_attachments_in_convo": "Прятать вложения в разговорах",
"hide_attachments_in_tl": "Прятать вложения в ленте",
"hide_isp": "Скрыть серверную панель",
"import_followers_from_a_csv_file": "Импортировать читаемых из файла .csv",
"import_theme": "Загрузить Тему",
"inputRadius": "Поля ввода",
"checkboxRadius": "Чекбоксы",
"interface": "Интерфейс",
"interfaceLanguage": "Язык интерфейса",
"limited_availability": "Не доступно в вашем браузере",
"links": "Ссылки",

12
src/modules/errors.js Normal file
View file

@ -0,0 +1,12 @@
import { capitalize } from 'lodash'
export function humanizeErrors (errors) {
return Object.entries(errors).reduce((errs, [k, val]) => {
let message = val.reduce((acc, message) => {
let key = capitalize(k.replace(/_/g, ' '))
return acc + [key, message].join(' ') + '. '
}, '')
return [...errs, message]
}, [])
}

View file

@ -1,6 +1,8 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { compact, map, each, merge } from 'lodash'
import { set } from 'vue'
import oauthApi from '../services/new_api/oauth'
import {humanizeErrors} from './errors'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
@ -46,15 +48,28 @@ export const mutations = {
setColor (state, { user: {id}, highlighted }) {
const user = state.usersObject[id]
set(user, 'highlight', highlighted)
},
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
},
signUpSuccess (state) {
state.signUpPending = false
},
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
}
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
loggingIn: false,
users: [],
usersObject: {}
usersObject: {},
signUpPending: false,
signUpErrors: []
}
const users = {
@ -80,6 +95,34 @@ const users = {
store.commit('setUserForStatus', status)
})
},
async signUp (store, userInfo) {
store.commit('signUpPending')
let rootState = store.rootState
let response = await rootState.api.backendInteractor.register(userInfo)
if (response.ok) {
const data = {
oauth: rootState.oauth,
instance: rootState.instance.server
}
let app = await oauthApi.getOrCreateApp(data)
let result = await oauthApi.getTokenWithCredentials({
app,
instance: data.instance,
username: userInfo.username,
password: userInfo.password
})
store.commit('signUpSuccess')
store.commit('setToken', result.access_token)
store.dispatch('loginUser', result.access_token)
} else {
let data = await response.json()
let errors = humanizeErrors(JSON.parse(data.error))
store.commit('signUpFailure', errors)
throw Error(errors)
}
},
logout (store) {
store.commit('clearCurrentUser')
store.commit('setToken', false)

981
yarn.lock

File diff suppressed because it is too large Load diff