From e822fdaeee90cb7c70d5678f19249198cd7aae8c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 7 Jun 2016 22:34:02 +0200 Subject: Use ng2-file-upload instead of jquery and add tags support to the video upload form --- client/.bootstraprc | 4 +- client/config/webpack.common.js | 8 +- client/package.json | 6 +- client/src/app/shared/search/search-field.type.ts | 2 +- client/src/app/shared/search/search.component.ts | 3 +- client/src/app/shared/users/auth.service.ts | 6 +- .../app/videos/video-add/video-add.component.html | 64 ++++++-- .../app/videos/video-add/video-add.component.scss | 27 +++- .../app/videos/video-add/video-add.component.ts | 162 ++++++++++++++------- client/src/vendor.ts | 4 +- client/tsconfig.json | 2 - client/typings.json | 2 - server/initializers/constants.js | 4 +- server/middlewares/reqValidators/videos.js | 2 +- server/models/videos.js | 1 + 15 files changed, 201 insertions(+), 96 deletions(-) diff --git a/client/.bootstraprc b/client/.bootstraprc index 76a0bdb7b..dd6c2128c 100644 --- a/client/.bootstraprc +++ b/client/.bootstraprc @@ -86,7 +86,7 @@ styles: breadcrumbs: false pagination: true pager: false - labels: false + labels: true badges: false jumbotron: false thumbnails: true @@ -112,7 +112,7 @@ styles: ### Bootstrap scripts scripts: transition: false - alert: true + alert: false button: false carousel: false collapse: false diff --git a/client/config/webpack.common.js b/client/config/webpack.common.js index 7f1da74b9..9d05668e2 100644 --- a/client/config/webpack.common.js +++ b/client/config/webpack.common.js @@ -68,7 +68,7 @@ module.exports = { root: helpers.root('src'), // remove other default values - modulesDirectories: [ 'node_modules', 'node_modules/blueimp-file-upload/js/vendor' ], + modulesDirectories: [ 'node_modules' ], packageAlias: 'browser' @@ -246,12 +246,6 @@ module.exports = { chunksSortMode: 'dependency' }), - new webpack.ProvidePlugin({ - jQuery: 'jquery', - $: 'jquery', - jquery: 'jquery' - }) - ], /* diff --git a/client/package.json b/client/package.json index d2d039437..cd8afcc98 100644 --- a/client/package.json +++ b/client/package.json @@ -28,7 +28,6 @@ "@angular/router-deprecated": "2.0.0-rc.1", "angular-pipes": "^2.0.0", "awesome-typescript-loader": "^0.17.0", - "blueimp-file-upload": "^9.12.1", "bootstrap-loader": "^1.0.8", "bootstrap-sass": "^3.3.6", "compression-webpack-plugin": "^0.3.1", @@ -40,10 +39,9 @@ "es6-shim": "^0.35.0", "file-loader": "^0.8.5", "html-webpack-plugin": "^2.19.0", - "jquery": "^2.2.3", - "jquery.ui.widget": "^1.10.3", "json-loader": "^0.5.4", "ng2-bootstrap": "^1.0.16", + "ng2-file-upload": "^1.0.3", "node-sass": "^3.7.0", "normalize.css": "^4.1.1", "raw-loader": "^0.5.1", @@ -75,4 +73,4 @@ "bundles/" ] } -} \ No newline at end of file +} diff --git a/client/src/app/shared/search/search-field.type.ts b/client/src/app/shared/search/search-field.type.ts index 846236290..5228ee68a 100644 --- a/client/src/app/shared/search/search-field.type.ts +++ b/client/src/app/shared/search/search-field.type.ts @@ -1 +1 @@ -export type SearchField = "name" | "author" | "podUrl" | "magnetUri"; +export type SearchField = "name" | "author" | "podUrl" | "magnetUri" | "tags"; diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts index 31f8b1535..c14c2d99c 100644 --- a/client/src/app/shared/search/search.component.ts +++ b/client/src/app/shared/search/search.component.ts @@ -18,7 +18,8 @@ export class SearchComponent { name: 'Name', author: 'Author', podUrl: 'Pod Url', - magnetUri: 'Magnet Uri' + magnetUri: 'Magnet Uri', + tags: 'Tags' }; searchCriterias: Search = { field: 'name', diff --git a/client/src/app/shared/users/auth.service.ts b/client/src/app/shared/users/auth.service.ts index 720037563..1c822c1e1 100644 --- a/client/src/app/shared/users/auth.service.ts +++ b/client/src/app/shared/users/auth.service.ts @@ -43,7 +43,11 @@ export class AuthService { } getRequestHeader() { - return new Headers({ 'Authorization': `${this.getTokenType()} ${this.getToken()}` }); + return new Headers({ 'Authorization': this.getRequestHeaderValue() }); + } + + getRequestHeaderValue() { + return `${this.getTokenType()} ${this.getToken()}`; } getToken() { diff --git a/client/src/app/videos/video-add/video-add.component.html b/client/src/app/videos/video-add/video-add.component.html index cbe274e8a..6b2eb9377 100644 --- a/client/src/app/videos/video-add/video-add.component.html +++ b/client/src/app/videos/video-add/video-add.component.html @@ -2,42 +2,74 @@
{{ error }}
-
+
- + -
- Name is required +
+ A name is required and should be between 3 and 50 characters long
-
+ + +
+ A tag should be between 2 and 10 characters long +
+
+ +
+
+ {{ tag }} + x +
+
+ +
+ +
Select the video... - +
+
- {{ fileToUpload.name }} +
+
+ {{ filename }} + +
-
- A description is required +
+ A description is required and should be between 3 and 250 characters long
-
- {{ progressBar.value | bytes }} / {{ progressBar.max | bytes }} +
+
- +
+ +
diff --git a/client/src/app/videos/video-add/video-add.component.scss b/client/src/app/videos/video-add/video-add.component.scss index 01195f017..d66df2fd4 100644 --- a/client/src/app/videos/video-add/video-add.component.scss +++ b/client/src/app/videos/video-add/video-add.component.scss @@ -1,6 +1,7 @@ .btn-file { position: relative; overflow: hidden; + display: block; } .btn-file input[type=file] { @@ -28,6 +29,28 @@ margin-bottom: 10px; } -#progress { - margin-bottom: 10px; +div.tags { + height: 40px; + font-size: 20px; + margin-top: 20px; + + .tag { + margin-right: 10px; + + .remove { + cursor: pointer; + } + } +} + +div.file-to-upload { + height: 40px; + + .glyphicon-remove { + cursor: pointer; + } +} + +div.progress { + // height: 40px; } diff --git a/client/src/app/videos/video-add/video-add.component.ts b/client/src/app/videos/video-add/video-add.component.ts index 144879a54..2b45ea125 100644 --- a/client/src/app/videos/video-add/video-add.component.ts +++ b/client/src/app/videos/video-add/video-add.component.ts @@ -1,29 +1,31 @@ -/// -/// - +import { Control, ControlGroup, Validators } from '@angular/common'; import { Component, ElementRef, OnInit } from '@angular/core'; import { Router } from '@angular/router-deprecated'; import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'; import { PROGRESSBAR_DIRECTIVES } from 'ng2-bootstrap/components/progressbar'; +import { FileSelectDirective, FileUploader } from 'ng2-file-upload/ng2-file-upload'; -import { AuthService, User } from '../../shared'; +import { AuthService } from '../../shared'; @Component({ selector: 'my-videos-add', styles: [ require('./video-add.component.scss') ], template: require('./video-add.component.html'), - directives: [ PROGRESSBAR_DIRECTIVES ], + directives: [ FileSelectDirective, PROGRESSBAR_DIRECTIVES ], pipes: [ BytesPipe ] }) export class VideoAddComponent implements OnInit { + currentTag: string; // Tag the user is writing in the input error: string = null; - fileToUpload: any; - progressBar: { value: number; max: number; } = { value: 0, max: 0 }; - user: User; - - private form: any; + videoForm: ControlGroup; + uploader: FileUploader; + video = { + name: '', + tags: [], + description: '' + }; constructor( private authService: AuthService, @@ -31,52 +33,108 @@ export class VideoAddComponent implements OnInit { private router: Router ) {} + get filename() { + if (this.uploader.queue.length === 0) { + return null; + } + + return this.uploader.queue[0].file.name; + } + + get isTagsInputDisabled () { + return this.video.tags.length >= 3; + } + + getInvalidFieldsTitle() { + let title = ''; + const nameControl = this.videoForm.controls['name']; + const descriptionControl = this.videoForm.controls['description']; + + if (!nameControl.valid) { + title += 'A name is required\n'; + } + + if (this.video.tags.length === 0) { + title += 'At least one tag is required\n'; + } + + if (this.filename === null) { + title += 'A file is required\n'; + } + + if (!descriptionControl.valid) { + title += 'A description is required\n'; + } + + return title; + } + ngOnInit() { - this.user = User.load(); - jQuery(this.elementRef.nativeElement).find('#videofile').fileupload({ + this.videoForm = new ControlGroup({ + name: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(50) ])), + description: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(250) ])), + tags: new Control('', Validators.pattern('^[a-zA-Z0-9]{2,10}$')) + }); + + + this.uploader = new FileUploader({ + authToken: this.authService.getRequestHeaderValue(), + queueLimit: 1, url: '/api/v1/videos', - dataType: 'json', - singleFileUploads: true, - multipart: true, - autoUpload: false, - - add: (e, data) => { - this.form = data; - this.fileToUpload = data['files'][0]; - }, - - progressall: (e, data) => { - this.progressBar.value = data.loaded; - // The server is a little bit slow to answer (has to seed the video) - // So we add more time to the progress bar (+10%) - this.progressBar.max = data.total + (0.1 * data.total); - }, - - done: (e, data) => { - this.progressBar.value = this.progressBar.max; - console.log('Video uploaded.'); - - // Print all the videos once it's finished - this.router.navigate(['VideosList']); - }, - - fail: (e, data) => { - const xhr = data.jqXHR; - if (xhr.status === 400) { - this.error = xhr.responseText; - } else { - this.error = 'Unknow error'; - } - - console.error(data); - } + removeAfterUpload: true }); + + this.uploader.onBuildItemForm = (item, form) => { + form.append('name', this.video.name); + form.append('description', this.video.description); + + for (let i = 0; i < this.video.tags.length; i++) { + form.append(`tags[${i}]`, this.video.tags[i]); + } + }; + } + + onTagKeyPress(event: KeyboardEvent) { + // Enter press + if (event.keyCode === 13) { + // Check if the tag is valid and does not already exist + if ( + this.currentTag !== '' && + this.videoForm.controls['tags'].valid && + this.video.tags.indexOf(this.currentTag) === -1 + ) { + this.video.tags.push(this.currentTag); + this.currentTag = ''; + } + } + } + + removeFile() { + this.uploader.clearQueue(); + } + + removeTag(tag: string) { + this.video.tags.splice(this.video.tags.indexOf(tag), 1); } - uploadFile() { - this.error = null; - this.form.formData = jQuery(this.elementRef.nativeElement).find('form').serializeArray(); - this.form.headers = this.authService.getRequestHeader().toJSON(); - this.form.submit(); + upload() { + const item = this.uploader.queue[0]; + // TODO: wait for https://github.com/valor-software/ng2-file-upload/pull/242 + item.alias = 'videofile'; + + item.onSuccess = () => { + console.log('Video uploaded.'); + + // Print all the videos once it's finished + this.router.navigate(['VideosList']); + }; + + item.onError = (response: string, status: number) => { + this.error = (status === 400) ? response : 'Unknow error'; + console.error(this.error); + }; + + + this.uploader.uploadAll(); } } diff --git a/client/src/vendor.ts b/client/src/vendor.ts index 496f44cf6..437d05822 100644 --- a/client/src/vendor.ts +++ b/client/src/vendor.ts @@ -18,7 +18,5 @@ import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/mergeMap'; -import 'jquery'; import 'bootstrap-loader'; -import 'jquery.ui.widget/jquery.ui.widget'; -import 'blueimp-file-upload'; +import 'ng2-file-upload'; diff --git a/client/tsconfig.json b/client/tsconfig.json index 3b903f8c8..fdcf742ea 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -65,8 +65,6 @@ "src/vendor.ts", "typings/globals/es6-shim/index.d.ts", "typings/globals/jasmine/index.d.ts", - "typings/globals/jquery.fileupload/index.d.ts", - "typings/globals/jquery/index.d.ts", "typings/globals/node/index.d.ts", "typings/index.d.ts" ] diff --git a/client/typings.json b/client/typings.json index ff8b56a48..9a8891f25 100644 --- a/client/typings.json +++ b/client/typings.json @@ -2,8 +2,6 @@ "globalDependencies": { "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654", "jasmine": "registry:dt/jasmine#2.2.0+20160412134438", - "jquery": "registry:dt/jquery#1.10.0+20160417213236", - "jquery.fileupload": "registry:dt/jquery.fileupload#5.40.1+20160316155526", "node": "registry:dt/node#4.0.0+20160509154515" } } diff --git a/server/initializers/constants.js b/server/initializers/constants.js index 6fa322010..22cbb1361 100644 --- a/server/initializers/constants.js +++ b/server/initializers/constants.js @@ -41,8 +41,8 @@ const THUMBNAILS_SIZE = '200x110' const THUMBNAILS_STATIC_PATH = '/static/thumbnails' const VIDEOS_CONSTRAINTS_FIELDS = { - NAME: { min: 1, max: 50 }, // Length - DESCRIPTION: { min: 1, max: 250 }, // Length + NAME: { min: 3, max: 50 }, // Length + DESCRIPTION: { min: 3, max: 250 }, // Length MAGNET_URI: { min: 10 }, // Length DURATION: { min: 1, max: 7200 }, // Number AUTHOR: { min: 3, max: 20 }, // Length diff --git a/server/middlewares/reqValidators/videos.js b/server/middlewares/reqValidators/videos.js index 3618e4716..f31fd93a2 100644 --- a/server/middlewares/reqValidators/videos.js +++ b/server/middlewares/reqValidators/videos.js @@ -32,7 +32,7 @@ function videosAdd (req, res, next) { } if (!customValidators.isVideoDurationValid(duration)) { - return res.status(400).send('Duration of the video file is too big (max: ' + constants.MAXIMUM_VIDEO_DURATION + 's).') + return res.status(400).send('Duration of the video file is too big (max: ' + constants.VIDEOS_CONSTRAINTS_FIELDS.DURATION.max + 's).') } videoFile.duration = duration diff --git a/server/models/videos.js b/server/models/videos.js index d6b743c7c..c177b414c 100644 --- a/server/models/videos.js +++ b/server/models/videos.js @@ -12,6 +12,7 @@ const port = config.get('webserver.port') // --------------------------------------------------------------------------- +// TODO: add indexes on searchable columns const videosSchema = mongoose.Schema({ name: String, namePath: String, -- cgit v1.2.3