Add ability to update the user display name/description

pull/525/head
Chocobozzz 2018-04-26 10:03:40 +02:00
parent d62cf3234c
commit ed56ad1193
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 215 additions and 19 deletions

View File

@ -5,7 +5,7 @@
</a>
<div class="logged-in-info">
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.username }}</a>
<a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
<div class="logged-in-email">{{ user.email }}</div>
</div>
@ -14,8 +14,8 @@
<ul *dropdownMenu class="dropdown-menu">
<li>
<a i18n routerLink="/my-account/settings" class="dropdown-item" title="My account">
My account
<a routerLink="/my-account/settings" class="dropdown-item" title="My settings">
My settings
</a>
<a (click)="logout($event)" class="dropdown-item" title="Log out" href="#">

View File

@ -1 +0,0 @@
export * from './my-account-details.component'

View File

@ -0,0 +1 @@
export * from './my-account-profile.component'

View File

@ -0,0 +1,24 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="updateMyProfile()" [formGroup]="form">
<label for="display-name">Display name</label>
<input
type="text" id="display-name"
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
>
<div *ngIf="formErrors['display-name']" class="form-error">
{{ formErrors['display-name'] }}
</div>
<label for="description">Description</label>
<textarea
id="description" formControlName="description"
[ngClass]="{ 'input-error': formErrors['description'] }"
></textarea>
<div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
</div>
<input type="submit" value="Update my profile" [disabled]="!form.valid">
</form>

View File

@ -0,0 +1,23 @@
@import '_variables';
@import '_mixins';
input[type=text] {
@include peertube-input-text(340px);
display: block;
margin-bottom: 15px;
}
textarea {
@include peertube-textarea(500px, 150px);
display: block;
}
input[type=submit] {
@include peertube-button;
@include orange-button;
margin-top: 15px;
}

View File

@ -0,0 +1,65 @@
import { Component, Input, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { NotificationsService } from 'angular2-notifications'
import { FormReactive, USER_DESCRIPTION, USER_DISPLAY_NAME, UserService } from '../../../shared'
import { User } from '@app/shared'
@Component({
selector: 'my-account-profile',
templateUrl: './my-account-profile.component.html',
styleUrls: [ './my-account-profile.component.scss' ]
})
export class MyAccountProfileComponent extends FormReactive implements OnInit {
@Input() user: User = null
error: string = null
form: FormGroup
formErrors = {
'display-name': '',
'description': ''
}
validationMessages = {
'display-name': USER_DISPLAY_NAME.MESSAGES,
'description': USER_DESCRIPTION.MESSAGES
}
constructor (
private formBuilder: FormBuilder,
private notificationsService: NotificationsService,
private userService: UserService
) {
super()
}
buildForm () {
this.form = this.formBuilder.group({
'display-name': [ this.user.account.displayName, USER_DISPLAY_NAME.VALIDATORS ],
'description': [ this.user.account.description, USER_DESCRIPTION.VALIDATORS ]
})
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
}
ngOnInit () {
this.buildForm()
}
updateMyProfile () {
const displayName = this.form.value['display-name']
const description = this.form.value['description']
this.error = null
this.userService.updateMyProfile({ displayName, description }).subscribe(
() => {
this.user.account.displayName = displayName
this.user.account.description = description
this.notificationsService.success('Success', 'Profile updated.')
},
err => this.error = err.message
)
}
}

View File

@ -17,8 +17,13 @@
<span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
</div>
<div class="account-title">Account settings</div>
<ng-template [ngIf]="user && user.account">
<div class="account-title">Profile</div>
<my-account-profile [user]="user"></my-account-profile>
</ng-template>
<div class="account-title">Password</div>
<my-account-change-password></my-account-change-password>
<div class="account-title">Video settings</div>
<my-account-details [user]="user"></my-account-details>
<my-account-video-settings [user]="user"></my-account-video-settings>

View File

@ -0,0 +1 @@
export * from './my-account-video-settings.component'

View File

@ -6,11 +6,11 @@ import { AuthService } from '../../../core'
import { FormReactive, User, UserService } from '../../../shared'
@Component({
selector: 'my-account-details',
templateUrl: './my-account-details.component.html',
styleUrls: [ './my-account-details.component.scss' ]
selector: 'my-account-video-settings',
templateUrl: './my-account-video-settings.component.html',
styleUrls: [ './my-account-video-settings.component.scss' ]
})
export class MyAccountDetailsComponent extends FormReactive implements OnInit {
export class MyAccountVideoSettingsComponent extends FormReactive implements OnInit {
@Input() user: User = null
form: FormGroup
@ -47,7 +47,7 @@ export class MyAccountDetailsComponent extends FormReactive implements OnInit {
autoPlayVideo
}
this.userService.updateMyDetails(details).subscribe(
this.userService.updateMyProfile(details).subscribe(
() => {
this.notificationsService.success('Success', 'Information updated.')

View File

@ -1,6 +1,6 @@
<div class="row">
<div class="sub-menu">
<a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My account</a>
<a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
<a routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
</div>

View File

@ -2,10 +2,11 @@ import { NgModule } from '@angular/core'
import { SharedModule } from '../shared'
import { MyAccountRoutingModule } from './my-account-routing.module'
import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
import { MyAccountDetailsComponent } from './my-account-settings/my-account-details/my-account-details.component'
import { MyAccountVideoSettingsComponent } from './my-account-settings/my-account-video-settings/my-account-video-settings.component'
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
import { MyAccountComponent } from './my-account.component'
import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
import { MyAccountProfileComponent } from '@app/my-account/my-account-settings/my-account-profile/my-account-profile.component'
@NgModule({
imports: [
@ -17,7 +18,8 @@ import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.
MyAccountComponent,
MyAccountSettingsComponent,
MyAccountChangePasswordComponent,
MyAccountDetailsComponent,
MyAccountVideoSettingsComponent,
MyAccountProfileComponent,
MyAccountVideosComponent
],

View File

@ -46,3 +46,27 @@ export const USER_ROLE = {
'required': 'User role is required.'
}
}
export const USER_DISPLAY_NAME = {
VALIDATORS: [
Validators.required,
Validators.minLength(3),
Validators.maxLength(120)
],
MESSAGES: {
'required': 'Display name is required.',
'minlength': 'Display name must be at least 3 characters long.',
'maxlength': 'Display name cannot be more than 120 characters long.'
}
}
export const USER_DESCRIPTION = {
VALIDATORS: [
Validators.required,
Validators.minLength(3),
Validators.maxLength(250)
],
MESSAGES: {
'required': 'Display name is required.',
'minlength': 'Display name must be at least 3 characters long.',
'maxlength': 'Display name cannot be more than 250 characters long.'
}
}

View File

@ -26,10 +26,10 @@ export class UserService {
.catch(res => this.restExtractor.handleError(res))
}
updateMyDetails (details: UserUpdateMe) {
updateMyProfile (profile: UserUpdateMe) {
const url = UserService.BASE_USERS_URL + 'me'
return this.authHttp.put(url, details)
return this.authHttp.put(url, profile)
.map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res))
}

View File

@ -303,6 +303,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
await sequelizeTypescript.transaction(async t => {
await user.save({ transaction: t })
if (body.displayName !== undefined) user.Account.name = body.displayName
if (body.description !== undefined) user.Account.description = body.description
await user.Account.save({ transaction: t })

View File

@ -22,6 +22,10 @@ function isUserUsernameValid (value: string) {
return exists(value) && validator.matches(value, new RegExp(`^[a-z0-9._]{${min},${max}}$`))
}
function isUserDisplayNameValid (value: string) {
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.NAME))
}
function isUserDescriptionValid (value: string) {
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
}
@ -60,6 +64,7 @@ export {
isUserUsernameValid,
isUserNSFWPolicyValid,
isUserAutoPlayVideoValid,
isUserDisplayNameValid,
isUserDescriptionValid,
isAvatarFile
}

View File

@ -180,9 +180,10 @@ const CONFIG = {
const CONSTRAINTS_FIELDS = {
USERS: {
NAME: { min: 3, max: 120 }, // Length
DESCRIPTION: { min: 3, max: 250 }, // Length
USERNAME: { min: 3, max: 20 }, // Length
PASSWORD: { min: 6, max: 255 }, // Length
DESCRIPTION: { min: 3, max: 250 }, // Length
VIDEO_QUOTA: { min: -1 }
},
VIDEO_ABUSES: {

View File

@ -7,7 +7,7 @@ import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
import {
isAvatarFile,
isUserAutoPlayVideoValid,
isUserDescriptionValid,
isUserDescriptionValid, isUserDisplayNameValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
@ -98,6 +98,7 @@ const usersUpdateValidator = [
]
const usersUpdateMeValidator = [
body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),

View File

@ -76,6 +76,22 @@ describe('Test users with multiple servers', function () {
await wait(5000)
})
it('Should be able to update my display name', async function () {
this.timeout(10000)
await updateMyUser({
url: servers[0].url,
accessToken: servers[0].accessToken,
displayName: 'my super display name'
})
const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
user = res.body
expect(user.account.displayName).to.equal('my super display name')
await wait(5000)
})
it('Should be able to update my description', async function () {
this.timeout(10000)
@ -87,6 +103,7 @@ describe('Test users with multiple servers', function () {
const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
user = res.body
expect(user.account.displayName).to.equal('my super display name')
expect(user.account.description).to.equal('my super description updated')
await wait(5000)
@ -111,7 +128,7 @@ describe('Test users with multiple servers', function () {
await wait(5000)
})
it('Should have updated my avatar and my description on other servers too', async function () {
it('Should have updated my profile on other servers too', async function () {
for (const server of servers) {
const resAccounts = await getAccountsList(server.url, '-createdAt')
@ -122,6 +139,7 @@ describe('Test users with multiple servers', function () {
const rootServer1Get = resAccount.body as Account
expect(rootServer1Get.name).to.equal('root')
expect(rootServer1Get.host).to.equal('localhost:9001')
expect(rootServer1Get.displayName).to.equal('my super display name')
expect(rootServer1Get.description).to.equal('my super description updated')
await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')

View File

@ -172,6 +172,7 @@ describe('Test users', function () {
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.roleLabel).to.equal('User')
expect(user.id).to.be.a('number')
expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
@ -316,6 +317,7 @@ describe('Test users', function () {
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
@ -347,6 +349,7 @@ describe('Test users', function () {
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
@ -365,6 +368,25 @@ describe('Test users', function () {
await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.png')
})
it('Should be able to update my display name', async function () {
await updateMyUser({
url: server.url,
accessToken: accessTokenUser,
displayName: 'new display name'
})
const res = await getMyUserInformation(server.url, accessTokenUser)
const user = res.body
expect(user.username).to.equal('user_1')
expect(user.email).to.equal('updated@example.com')
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
expect(user.account.displayName).to.equal('new display name')
expect(user.account.description).to.be.null
})
it('Should be able to update my description', async function () {
await updateMyUser({
url: server.url,
@ -380,6 +402,7 @@ describe('Test users', function () {
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
expect(user.account.displayName).to.equal('new display name')
expect(user.account.description).to.equal('my super description updated')
})

View File

@ -132,6 +132,7 @@ function updateMyUser (options: {
nsfwPolicy?: NSFWPolicyType,
email?: string,
autoPlayVideo?: boolean
displayName?: string,
description?: string
}) {
const path = '/api/v1/users/me'
@ -142,6 +143,7 @@ function updateMyUser (options: {
if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
return makePutBodyRequest({
url: options.url,

View File

@ -1,6 +1,7 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface UserUpdateMe {
displayName?: string
description?: string
nsfwPolicy?: NSFWPolicyType
autoPlayVideo?: boolean