From 02c01341f4dae30ec6b81fcb644952393d73c4a8 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Wed, 5 Aug 2020 00:50:07 +0200 Subject: add ng-select for templatable select options - create select-tags component to replace ngx-chips - create select-options to factorize option selection in forms - create select-channel to simplify channel selection - refactor tags validation --- .../form-validators/video-validators.service.ts | 27 ++++++++++-- .../shared-forms/select-channel.component.html | 16 +++++++ .../shared-forms/select-channel.component.ts | 51 ++++++++++++++++++++++ .../shared-forms/select-options.component.html | 18 ++++++++ .../shared-forms/select-options.component.ts | 47 ++++++++++++++++++++ .../shared-forms/select-shared.component.scss | 20 +++++++++ .../shared/shared-forms/select-tags.component.html | 13 ++++++ .../shared/shared-forms/select-tags.component.scss | 3 ++ .../shared/shared-forms/select-tags.component.ts | 38 ++++++++++++++++ .../app/shared/shared-forms/shared-form.module.ts | 13 +++++- .../app/shared/shared-main/shared-main.module.ts | 7 ++- .../app/shared/shared-main/video/video.service.ts | 12 ++--- 12 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 client/src/app/shared/shared-forms/select-channel.component.html create mode 100644 client/src/app/shared/shared-forms/select-channel.component.ts create mode 100644 client/src/app/shared/shared-forms/select-options.component.html create mode 100644 client/src/app/shared/shared-forms/select-options.component.ts create mode 100644 client/src/app/shared/shared-forms/select-shared.component.scss create mode 100644 client/src/app/shared/shared-forms/select-tags.component.html create mode 100644 client/src/app/shared/shared-forms/select-tags.component.scss create mode 100644 client/src/app/shared/shared-forms/select-tags.component.ts (limited to 'client/src/app/shared') diff --git a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts b/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts index 9b24e4f62..c96e4ef66 100644 --- a/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts +++ b/client/src/app/shared/shared-forms/form-validators/video-validators.service.ts @@ -1,5 +1,5 @@ import { I18n } from '@ngx-translate/i18n-polyfill' -import { Validators } from '@angular/forms' +import { Validators, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms' import { Injectable } from '@angular/core' import { BuildFormValidator } from './form-validator.service' @@ -13,7 +13,8 @@ export class VideoValidatorsService { readonly VIDEO_IMAGE: BuildFormValidator readonly VIDEO_CHANNEL: BuildFormValidator readonly VIDEO_DESCRIPTION: BuildFormValidator - readonly VIDEO_TAGS: BuildFormValidator + readonly VIDEO_TAGS_ARRAY: BuildFormValidator + readonly VIDEO_TAG: BuildFormValidator readonly VIDEO_SUPPORT: BuildFormValidator readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator @@ -71,7 +72,7 @@ export class VideoValidatorsService { } } - this.VIDEO_TAGS = { + this.VIDEO_TAG = { VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ], MESSAGES: { 'minlength': this.i18n('A tag should be more than 2 characters long.'), @@ -79,6 +80,14 @@ export class VideoValidatorsService { } } + this.VIDEO_TAGS_ARRAY = { + VALIDATORS: [ Validators.maxLength(5), this.arrayTagLengthValidator() ], + MESSAGES: { + 'maxlength': this.i18n('A maximum of 5 tags can be used on a video.'), + 'arrayTagLength': this.i18n('A tag should be more than 2, and less than 30 characters long.') + } + } + this.VIDEO_SUPPORT = { VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ], MESSAGES: { @@ -99,4 +108,16 @@ export class VideoValidatorsService { MESSAGES: {} } } + + arrayTagLengthValidator (min = 2, max = 30): ValidatorFn { + return (control: AbstractControl): ValidationErrors => { + const array = control.value as Array + + if (array.every(e => e.length > min && e.length < max)) { + return null + } + + return { 'arrayTagLength': true } + } + } } diff --git a/client/src/app/shared/shared-forms/select-channel.component.html b/client/src/app/shared/shared-forms/select-channel.component.html new file mode 100644 index 000000000..897d13ee7 --- /dev/null +++ b/client/src/app/shared/shared-forms/select-channel.component.html @@ -0,0 +1,16 @@ + + + + {{ channel.label }} + + diff --git a/client/src/app/shared/shared-forms/select-channel.component.ts b/client/src/app/shared/shared-forms/select-channel.component.ts new file mode 100644 index 000000000..de98c8c0a --- /dev/null +++ b/client/src/app/shared/shared-forms/select-channel.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, forwardRef, ViewChild } from '@angular/core' +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' +import { Actor } from '../shared-main' + +@Component({ + selector: 'my-select-channel', + styleUrls: [ './select-shared.component.scss' ], + templateUrl: './select-channel.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectChannelComponent), + multi: true + } + ] +}) +export class SelectChannelComponent implements ControlValueAccessor { + @Input() items: { id: number, label: string, support: string, avatarPath?: string }[] = [] + + selectedId: number + + // ng-select options + bindLabel = 'label' + bindValue = 'id' + clearable = false + searchable = false + + get channels () { + return this.items.map(c => Object.assign(c, { + avatarPath: c.avatarPath ? c.avatarPath : Actor.GET_DEFAULT_AVATAR_URL() + })) + } + + propagateChange = (_: any) => { /* empty */ } + + writeValue (id: number) { + this.selectedId = id + } + + registerOnChange (fn: (_: any) => void) { + this.propagateChange = fn + } + + registerOnTouched () { + // Unused + } + + onModelChange () { + this.propagateChange(this.selectedId) + } +} diff --git a/client/src/app/shared/shared-forms/select-options.component.html b/client/src/app/shared/shared-forms/select-options.component.html new file mode 100644 index 000000000..fda0c2c56 --- /dev/null +++ b/client/src/app/shared/shared-forms/select-options.component.html @@ -0,0 +1,18 @@ + + + {{ item.label }} + +
+ {{ item.description }} +
+
+
diff --git a/client/src/app/shared/shared-forms/select-options.component.ts b/client/src/app/shared/shared-forms/select-options.component.ts new file mode 100644 index 000000000..09f7df53b --- /dev/null +++ b/client/src/app/shared/shared-forms/select-options.component.ts @@ -0,0 +1,47 @@ +import { Component, Input, forwardRef } from '@angular/core' +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' + +export type SelectOptionsItem = { id: number | string, label: string, description?: string } + +@Component({ + selector: 'my-select-options', + styleUrls: [ './select-shared.component.scss' ], + templateUrl: './select-options.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectOptionsComponent), + multi: true + } + ] +}) +export class SelectOptionsComponent implements ControlValueAccessor { + @Input() items: SelectOptionsItem[] = [] + @Input() clearable = false + @Input() searchable = false + @Input() bindValue = 'id' + @Input() groupBy: string + + selectedId: number | string + + // ng-select options + bindLabel = 'label' + + propagateChange = (_: any) => { /* empty */ } + + writeValue (id: number | string) { + this.selectedId = id + } + + registerOnChange (fn: (_: any) => void) { + this.propagateChange = fn + } + + registerOnTouched () { + // Unused + } + + onModelChange () { + this.propagateChange(this.selectedId) + } +} diff --git a/client/src/app/shared/shared-forms/select-shared.component.scss b/client/src/app/shared/shared-forms/select-shared.component.scss new file mode 100644 index 000000000..4f231d28a --- /dev/null +++ b/client/src/app/shared/shared-forms/select-shared.component.scss @@ -0,0 +1,20 @@ +$width-size: auto; + +ng-select { + width: $width-size; + @media screen and (max-width: $width-size) { + width: 100%; + } +} + +// make sure the image is vertically adjusted +ng-select ::ng-deep .ng-value-label img { + position: relative; + top: -1px; +} + +ng-select ::ng-deep img { + border-radius: 50%; + height: 20px; + width: 20px; +} diff --git a/client/src/app/shared/shared-forms/select-tags.component.html b/client/src/app/shared/shared-forms/select-tags.component.html new file mode 100644 index 000000000..0609c9d20 --- /dev/null +++ b/client/src/app/shared/shared-forms/select-tags.component.html @@ -0,0 +1,13 @@ + + diff --git a/client/src/app/shared/shared-forms/select-tags.component.scss b/client/src/app/shared/shared-forms/select-tags.component.scss new file mode 100644 index 000000000..ad76bc7ee --- /dev/null +++ b/client/src/app/shared/shared-forms/select-tags.component.scss @@ -0,0 +1,3 @@ +ng-select ::ng-deep .ng-arrow-wrapper { + display: none; +} diff --git a/client/src/app/shared/shared-forms/select-tags.component.ts b/client/src/app/shared/shared-forms/select-tags.component.ts new file mode 100644 index 000000000..2e07d7e8f --- /dev/null +++ b/client/src/app/shared/shared-forms/select-tags.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, forwardRef } from '@angular/core' +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' + +@Component({ + selector: 'my-select-tags', + styleUrls: [ './select-shared.component.scss', './select-tags.component.scss' ], + templateUrl: './select-tags.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SelectTagsComponent), + multi: true + } + ] +}) +export class SelectTagsComponent implements ControlValueAccessor { + @Input() items: string[] = [] + @Input() _items: string[] = [] + + propagateChange = (_: any) => { /* empty */ } + + writeValue (items: string[]) { + this._items = items + this.propagateChange(this._items) + } + + registerOnChange (fn: (_: any) => void) { + this.propagateChange = fn + } + + registerOnTouched () { + // Unused + } + + onModelChange () { + this.propagateChange(this._items) + } +} diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts index ba33704cf..19d812948 100644 --- a/client/src/app/shared/shared-forms/shared-form.module.ts +++ b/client/src/app/shared/shared-forms/shared-form.module.ts @@ -28,6 +28,9 @@ import { PreviewUploadComponent } from './preview-upload.component' import { ReactiveFileComponent } from './reactive-file.component' import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' import { TimestampInputComponent } from './timestamp-input.component' +import { SelectChannelComponent } from './select-channel.component' +import { SelectOptionsComponent } from './select-options.component' +import { SelectTagsComponent } from './select-tags.component' @NgModule({ imports: [ @@ -45,7 +48,10 @@ import { TimestampInputComponent } from './timestamp-input.component' PreviewUploadComponent, ReactiveFileComponent, TextareaAutoResizeDirective, - TimestampInputComponent + TimestampInputComponent, + SelectChannelComponent, + SelectOptionsComponent, + SelectTagsComponent ], exports: [ @@ -58,7 +64,10 @@ import { TimestampInputComponent } from './timestamp-input.component' PreviewUploadComponent, ReactiveFileComponent, TextareaAutoResizeDirective, - TimestampInputComponent + TimestampInputComponent, + SelectChannelComponent, + SelectOptionsComponent, + SelectTagsComponent ], providers: [ diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 22a207e51..a4d18d562 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts @@ -17,6 +17,7 @@ import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' +import { NgSelectModule } from '@ng-select/ng-select' import { I18n } from '@ngx-translate/i18n-polyfill' import { SharedGlobalIconModule } from '../shared-icons' import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account' @@ -55,6 +56,8 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' MultiSelectModule, InputSwitchModule, + NgSelectModule, + SharedGlobalIconModule ], @@ -134,7 +137,9 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' TopMenuDropdownComponent, UserQuotaComponent, - UserNotificationsComponent + UserNotificationsComponent, + + NgSelectModule ], providers: [ diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index edaefa9f2..978f775bf 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -339,23 +339,25 @@ export class VideoService implements VideosProvider { const base = [ { id: VideoPrivacy.PRIVATE, - label: this.i18n('Only I can see this video') + description: this.i18n('Only I can see this video') }, { id: VideoPrivacy.UNLISTED, - label: this.i18n('Only people with the private link can see this video') + description: this.i18n('Only shareable via a private link') }, { id: VideoPrivacy.PUBLIC, - label: this.i18n('Anyone can see this video') + description: this.i18n('Anyone can see this video') }, { id: VideoPrivacy.INTERNAL, - label: this.i18n('Only users of this instance can see this video') + description: this.i18n('Only users of this instance can see this video') } ] - return base.filter(o => !!privacies.find(p => p.id === o.id)) + return base + .filter(o => !!privacies.find(p => p.id === o.id)) // filter down to privacies that where in the input + .map(o => ({ ...privacies[o.id - 1], ...o })) // merge the input privacies that contain a label, and extend them with a description } nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { -- cgit v1.2.3