diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-08-05 00:50:07 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-08-11 09:03:39 +0200 |
commit | 02c01341f4dae30ec6b81fcb644952393d73c4a8 (patch) | |
tree | aca3f2b118bb123457fd38724be68fe877504c75 /client/src/app | |
parent | 766d13b4470de02d3d7bec94188260b89a356399 (diff) | |
download | PeerTube-02c01341f4dae30ec6b81fcb644952393d73c4a8.tar.gz PeerTube-02c01341f4dae30ec6b81fcb644952393d73c4a8.tar.zst PeerTube-02c01341f4dae30ec6b81fcb644952393d73c4a8.zip |
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
Diffstat (limited to 'client/src/app')
30 files changed, 359 insertions, 130 deletions
diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html index e20aef8fb..b36b1d2ae 100644 --- a/client/src/app/+search/search-filters.component.html +++ b/client/src/app/+search/search-filters.component.html | |||
@@ -144,12 +144,7 @@ | |||
144 | <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf"> | 144 | <button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf"> |
145 | Reset | 145 | Reset |
146 | </button> | 146 | </button> |
147 | <tag-input | 147 | <my-select-tags labelForId="tagsAllOf" id="tagsAllOf" [(ngModel)]="advancedSearch.tagsAllOf"></my-select-tags> |
148 | [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf" | ||
149 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
150 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" | ||
151 | [maxItems]="5" [modelAsStrings]="true" | ||
152 | ></tag-input> | ||
153 | </div> | 148 | </div> |
154 | 149 | ||
155 | <div class="form-group"> | 150 | <div class="form-group"> |
@@ -157,12 +152,7 @@ | |||
157 | <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf"> | 152 | <button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf"> |
158 | Reset | 153 | Reset |
159 | </button> | 154 | </button> |
160 | <tag-input | 155 | <my-select-tags labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags> |
161 | [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf" | ||
162 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | ||
163 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag" | ||
164 | [maxItems]="5" [modelAsStrings]="true" | ||
165 | ></tag-input> | ||
166 | </div> | 156 | </div> |
167 | 157 | ||
168 | <div class="form-group" *ngIf="isSearchTargetEnabled()"> | 158 | <div class="form-group" *ngIf="isSearchTargetEnabled()"> |
diff --git a/client/src/app/+search/search-filters.component.scss b/client/src/app/+search/search-filters.component.scss index a88a1c0b0..68ac6d021 100644 --- a/client/src/app/+search/search-filters.component.scss +++ b/client/src/app/+search/search-filters.component.scss | |||
@@ -65,5 +65,3 @@ input[type=submit] { | |||
65 | display: flex; | 65 | display: flex; |
66 | white-space: nowrap; | 66 | white-space: nowrap; |
67 | } | 67 | } |
68 | |||
69 | @include ng2-tags; | ||
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts index fc1db3258..13ad61647 100644 --- a/client/src/app/+search/search-filters.component.ts +++ b/client/src/app/+search/search-filters.component.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { ValidatorFn } from '@angular/forms' | ||
3 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
4 | import { VideoValidatorsService } from '@app/shared/shared-forms' | 3 | import { VideoValidatorsService } from '@app/shared/shared-forms' |
5 | import { AdvancedSearch } from '@app/shared/shared-search' | 4 | import { AdvancedSearch } from '@app/shared/shared-search' |
@@ -20,9 +19,6 @@ export class SearchFiltersComponent implements OnInit { | |||
20 | videoLicences: VideoConstant<number>[] = [] | 19 | videoLicences: VideoConstant<number>[] = [] |
21 | videoLanguages: VideoConstant<string>[] = [] | 20 | videoLanguages: VideoConstant<string>[] = [] |
22 | 21 | ||
23 | tagValidators: ValidatorFn[] | ||
24 | tagValidatorsMessages: { [ name: string ]: string } | ||
25 | |||
26 | publishedDateRanges: { id: string, label: string }[] = [] | 22 | publishedDateRanges: { id: string, label: string }[] = [] |
27 | sorts: { id: string, label: string }[] = [] | 23 | sorts: { id: string, label: string }[] = [] |
28 | durationRanges: { id: string, label: string }[] = [] | 24 | durationRanges: { id: string, label: string }[] = [] |
@@ -40,8 +36,6 @@ export class SearchFiltersComponent implements OnInit { | |||
40 | private videoValidatorsService: VideoValidatorsService, | 36 | private videoValidatorsService: VideoValidatorsService, |
41 | private serverService: ServerService | 37 | private serverService: ServerService |
42 | ) { | 38 | ) { |
43 | this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS | ||
44 | this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES | ||
45 | this.publishedDateRanges = [ | 39 | this.publishedDateRanges = [ |
46 | { | 40 | { |
47 | id: 'any_published_date', | 41 | id: 'any_published_date', |
diff --git a/client/src/app/+search/search.module.ts b/client/src/app/+search/search.module.ts index ee4f07ad1..e85ae07d0 100644 --- a/client/src/app/+search/search.module.ts +++ b/client/src/app/+search/search.module.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { TagInputModule } from 'ngx-chips' | ||
2 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
3 | import { SharedFormModule } from '@app/shared/shared-forms' | 2 | import { SharedFormModule } from '@app/shared/shared-forms' |
4 | import { SharedMainModule } from '@app/shared/shared-main' | 3 | import { SharedMainModule } from '@app/shared/shared-main' |
@@ -14,8 +13,6 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver' | |||
14 | 13 | ||
15 | @NgModule({ | 14 | @NgModule({ |
16 | imports: [ | 15 | imports: [ |
17 | TagInputModule, | ||
18 | |||
19 | SearchRoutingModule, | 16 | SearchRoutingModule, |
20 | 17 | ||
21 | SharedMainModule, | 18 | SharedMainModule, |
@@ -31,7 +28,6 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver' | |||
31 | ], | 28 | ], |
32 | 29 | ||
33 | exports: [ | 30 | exports: [ |
34 | TagInputModule, | ||
35 | SearchComponent | 31 | SearchComponent |
36 | ], | 32 | ], |
37 | 33 | ||
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html index 6a9e31b5a..6a07dafa7 100644 --- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.html | |||
@@ -8,11 +8,11 @@ | |||
8 | 8 | ||
9 | <div class="modal-body"> | 9 | <div class="modal-body"> |
10 | <label i18n for="language">Language</label> | 10 | <label i18n for="language">Language</label> |
11 | <div class="peertube-select-container"> | 11 | <div class="peertube-ng-select-container"> |
12 | <select id="language" formControlName="language" class="form-control"> | 12 | <ng-select |
13 | <option></option> | 13 | labelForId="language" [items]="videoCaptionLanguages" formControlName="language" |
14 | <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option> | 14 | bindLabel="label" bindValue="id" |
15 | </select> | 15 | ></ng-select> |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <div *ngIf="formErrors.language" class="form-error"> | 18 | <div *ngIf="formErrors.language" class="form-error"> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss index b257a16a9..0958b5f80 100644 --- a/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.scss | |||
@@ -1,8 +1,9 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .peertube-select-container { | 4 | label { |
5 | @include peertube-select-container(auto); | 5 | font-weight: $font-regular; |
6 | font-size: 100%; | ||
6 | } | 7 | } |
7 | 8 | ||
8 | .caption-file { | 9 | .caption-file { |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 4d3b84626..ae3413e79 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -28,11 +28,10 @@ | |||
28 | </ng-template> | 28 | </ng-template> |
29 | </my-help> | 29 | </my-help> |
30 | 30 | ||
31 | <tag-input | 31 | <my-select-tags labelForId="label-tags" formControlName="tags"></my-select-tags> |
32 | [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" | 32 | <div *ngIf="formErrors.tags" class="form-error"> |
33 | i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" | 33 | {{ formErrors.tags }} |
34 | formControlName="tags" [maxItems]="5" [modelAsStrings]="true" | 34 | </div> |
35 | ></tag-input> | ||
36 | </div> | 35 | </div> |
37 | 36 | ||
38 | <div class="form-group"> | 37 | <div class="form-group"> |
@@ -56,22 +55,15 @@ | |||
56 | 55 | ||
57 | <div class="col-video-edit"> | 56 | <div class="col-video-edit"> |
58 | <div class="form-group"> | 57 | <div class="form-group"> |
59 | <label i18n>Channel</label> | 58 | <label i18n for="channel">Channel</label> |
60 | <div class="peertube-select-container"> | 59 | <my-select-channel labelForId="channel" [items]="userVideoChannels" formControlName="channelId"></my-select-channel> |
61 | <select formControlName="channelId" class="form-control"> | ||
62 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | ||
63 | </select> | ||
64 | </div> | ||
65 | </div> | 60 | </div> |
66 | 61 | ||
67 | <div class="form-group"> | 62 | <div class="form-group"> |
68 | <label i18n for="category">Category</label> | 63 | <label i18n for="category">Category</label> |
69 | <div class="peertube-select-container"> | 64 | <my-select-options |
70 | <select id="category" formControlName="category" class="form-control"> | 65 | labelForId="category" [items]="videoCategories" formControlName="category" [clearable]="true" |
71 | <option></option> | 66 | ></my-select-options> |
72 | <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option> | ||
73 | </select> | ||
74 | </div> | ||
75 | 67 | ||
76 | <div *ngIf="formErrors.category" class="form-error"> | 68 | <div *ngIf="formErrors.category" class="form-error"> |
77 | {{ formErrors.category }} | 69 | {{ formErrors.category }} |
@@ -80,12 +72,9 @@ | |||
80 | 72 | ||
81 | <div class="form-group"> | 73 | <div class="form-group"> |
82 | <label i18n for="licence">Licence</label> | 74 | <label i18n for="licence">Licence</label> |
83 | <div class="peertube-select-container"> | 75 | <my-select-options |
84 | <select id="licence" formControlName="licence" class="form-control"> | 76 | labelForId="licence" [items]="videoLicences" formControlName="licence" [clearable]="true" |
85 | <option></option> | 77 | ></my-select-options> |
86 | <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option> | ||
87 | </select> | ||
88 | </div> | ||
89 | 78 | ||
90 | <div *ngIf="formErrors.licence" class="form-error"> | 79 | <div *ngIf="formErrors.licence" class="form-error"> |
91 | {{ formErrors.licence }} | 80 | {{ formErrors.licence }} |
@@ -94,12 +83,10 @@ | |||
94 | 83 | ||
95 | <div class="form-group"> | 84 | <div class="form-group"> |
96 | <label i18n for="language">Language</label> | 85 | <label i18n for="language">Language</label> |
97 | <div class="peertube-select-container"> | 86 | <my-select-options |
98 | <select id="language" formControlName="language" class="form-control"> | 87 | labelForId="language" [items]="videoLanguages" formControlName="language" |
99 | <option></option> | 88 | [clearable]="true" [searchable]="true" [groupBy]="'group'" |
100 | <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option> | 89 | ></my-select-options> |
101 | </select> | ||
102 | </div> | ||
103 | 90 | ||
104 | <div *ngIf="formErrors.language" class="form-error"> | 91 | <div *ngIf="formErrors.language" class="form-error"> |
105 | {{ formErrors.language }} | 92 | {{ formErrors.language }} |
@@ -108,13 +95,9 @@ | |||
108 | 95 | ||
109 | <div class="form-group"> | 96 | <div class="form-group"> |
110 | <label i18n for="privacy">Privacy</label> | 97 | <label i18n for="privacy">Privacy</label> |
111 | <div class="peertube-select-container"> | 98 | <my-select-options |
112 | <select id="privacy" formControlName="privacy" class="form-control"> | 99 | labelForId="privacy" [items]="videoPrivacies" formControlName="privacy" [clearable]="false" |
113 | <option></option> | 100 | ></my-select-options> |
114 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | ||
115 | <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option> | ||
116 | </select> | ||
117 | </div> | ||
118 | 101 | ||
119 | <div *ngIf="formErrors.privacy" class="form-error"> | 102 | <div *ngIf="formErrors.privacy" class="form-error"> |
120 | {{ formErrors.privacy }} | 103 | {{ formErrors.privacy }} |
@@ -136,7 +119,7 @@ | |||
136 | 119 | ||
137 | <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right"> | 120 | <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right"> |
138 | <ng-template ptTemplate="label"> | 121 | <ng-template ptTemplate="label"> |
139 | <ng-container i18n>This video contains mature or explicit content</ng-container> | 122 | <ng-container i18n>Contains sensitive content</ng-container> |
140 | </ng-template> | 123 | </ng-template> |
141 | 124 | ||
142 | <ng-template ptTemplate="help"> | 125 | <ng-template ptTemplate="help"> |
@@ -146,7 +129,7 @@ | |||
146 | 129 | ||
147 | <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right"> | 130 | <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right"> |
148 | <ng-template ptTemplate="label"> | 131 | <ng-template ptTemplate="label"> |
149 | <ng-container i18n>Wait transcoding before publishing the video</ng-container> | 132 | <ng-container i18n>Publish after transcoding</ng-container> |
150 | </ng-template> | 133 | </ng-template> |
151 | 134 | ||
152 | <ng-template ptTemplate="help"> | 135 | <ng-template ptTemplate="help"> |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index 69b907288..9caf009c5 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss | |||
@@ -161,8 +161,6 @@ p-calendar { | |||
161 | } | 161 | } |
162 | } | 162 | } |
163 | 163 | ||
164 | @include ng2-tags; | ||
165 | |||
166 | // columns for the video | 164 | // columns for the video |
167 | .col-video-edit { | 165 | .col-video-edit { |
168 | @include make-col-ready(); | 166 | @include make-col-ready(); |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 239e453ad..4cd3838de 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts | |||
@@ -8,6 +8,11 @@ import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-ma | |||
8 | import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' | 8 | import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' |
9 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 9 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
10 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 10 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
12 | import { forkJoin } from 'rxjs' | ||
13 | import { InstanceService } from '@app/shared/shared-instance' | ||
14 | |||
15 | type VideoLanguages = VideoConstant<string> & { group?: string } | ||
11 | 16 | ||
12 | @Component({ | 17 | @Component({ |
13 | selector: 'my-video-edit', | 18 | selector: 'my-video-edit', |
@@ -31,7 +36,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
31 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 36 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] |
32 | videoCategories: VideoConstant<number>[] = [] | 37 | videoCategories: VideoConstant<number>[] = [] |
33 | videoLicences: VideoConstant<number>[] = [] | 38 | videoLicences: VideoConstant<number>[] = [] |
34 | videoLanguages: VideoConstant<string>[] = [] | 39 | videoLanguages: VideoLanguages[] = [] |
35 | 40 | ||
36 | tagValidators: ValidatorFn[] | 41 | tagValidators: ValidatorFn[] |
37 | tagValidatorsMessages: { [ name: string ]: string } | 42 | tagValidatorsMessages: { [ name: string ]: string } |
@@ -56,12 +61,11 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
56 | private videoValidatorsService: VideoValidatorsService, | 61 | private videoValidatorsService: VideoValidatorsService, |
57 | private videoService: VideoService, | 62 | private videoService: VideoService, |
58 | private serverService: ServerService, | 63 | private serverService: ServerService, |
64 | private instanceService: InstanceService, | ||
59 | private i18nPrimengCalendarService: I18nPrimengCalendarService, | 65 | private i18nPrimengCalendarService: I18nPrimengCalendarService, |
66 | private i18n: I18n, | ||
60 | private ngZone: NgZone | 67 | private ngZone: NgZone |
61 | ) { | 68 | ) { |
62 | this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS | ||
63 | this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES | ||
64 | |||
65 | this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() | 69 | this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale() |
66 | this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() | 70 | this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone() |
67 | this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() | 71 | this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat() |
@@ -93,7 +97,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
93 | licence: this.videoValidatorsService.VIDEO_LICENCE, | 97 | licence: this.videoValidatorsService.VIDEO_LICENCE, |
94 | language: this.videoValidatorsService.VIDEO_LANGUAGE, | 98 | language: this.videoValidatorsService.VIDEO_LANGUAGE, |
95 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, | 99 | description: this.videoValidatorsService.VIDEO_DESCRIPTION, |
96 | tags: null, | 100 | tags: this.videoValidatorsService.VIDEO_TAGS_ARRAY, |
97 | previewfile: null, | 101 | previewfile: null, |
98 | support: this.videoValidatorsService.VIDEO_SUPPORT, | 102 | support: this.videoValidatorsService.VIDEO_SUPPORT, |
99 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, | 103 | schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT, |
@@ -126,11 +130,29 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
126 | .subscribe(res => this.videoCategories = res) | 130 | .subscribe(res => this.videoCategories = res) |
127 | this.serverService.getVideoLicences() | 131 | this.serverService.getVideoLicences() |
128 | .subscribe(res => this.videoLicences = res) | 132 | .subscribe(res => this.videoLicences = res) |
129 | this.serverService.getVideoLanguages() | 133 | forkJoin([ |
130 | .subscribe(res => this.videoLanguages = res) | 134 | this.instanceService.getAbout(), |
135 | this.serverService.getVideoLanguages() | ||
136 | ]).pipe(map(([ about, languages ]) => ({ about, languages }))) | ||
137 | .subscribe(res => { | ||
138 | this.videoLanguages = res.languages | ||
139 | .map(l => res.about.instance.languages.includes(l.id) | ||
140 | ? { ...l, group: this.i18n('Instance languages'), groupOrder: 0 } | ||
141 | : { ...l, group: this.i18n('All languages'), groupOrder: 1 }) | ||
142 | .sort((a, b) => a.groupOrder - b.groupOrder) | ||
143 | }) | ||
131 | 144 | ||
132 | this.serverService.getVideoPrivacies() | 145 | this.serverService.getVideoPrivacies() |
133 | .subscribe(privacies => this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)) | 146 | .subscribe(privacies => { |
147 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies) | ||
148 | if (this.schedulePublicationPossible) { | ||
149 | this.videoPrivacies.push({ | ||
150 | id: this.SPECIAL_SCHEDULED_PRIVACY, | ||
151 | label: this.i18n('Scheduled'), | ||
152 | description: this.i18n('Hide the video until a specific date') | ||
153 | }) | ||
154 | } | ||
155 | }) | ||
134 | 156 | ||
135 | this.serverConfig = this.serverService.getTmpConfig() | 157 | this.serverConfig = this.serverService.getTmpConfig() |
136 | this.serverService.getConfig() | 158 | this.serverService.getConfig() |
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts index d1bbbafe9..593114181 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.module.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.module.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import { TagInputModule } from 'ngx-chips' | ||
2 | import { CalendarModule } from 'primeng/calendar' | 1 | import { CalendarModule } from 'primeng/calendar' |
3 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
4 | import { SharedFormModule } from '@app/shared/shared-forms' | 3 | import { SharedFormModule } from '@app/shared/shared-forms' |
@@ -10,7 +9,6 @@ import { VideoEditComponent } from './video-edit.component' | |||
10 | 9 | ||
11 | @NgModule({ | 10 | @NgModule({ |
12 | imports: [ | 11 | imports: [ |
13 | TagInputModule, | ||
14 | CalendarModule, | 12 | CalendarModule, |
15 | 13 | ||
16 | SharedMainModule, | 14 | SharedMainModule, |
@@ -24,7 +22,6 @@ import { VideoEditComponent } from './video-edit.component' | |||
24 | ], | 22 | ], |
25 | 23 | ||
26 | exports: [ | 24 | exports: [ |
27 | TagInputModule, | ||
28 | CalendarModule, | 25 | CalendarModule, |
29 | 26 | ||
30 | SharedMainModule, | 27 | SharedMainModule, |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html index 5678f548f..8db37a293 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.html | |||
@@ -25,20 +25,16 @@ | |||
25 | 25 | ||
26 | <div class="form-group"> | 26 | <div class="form-group"> |
27 | <label i18n for="first-step-channel">Channel</label> | 27 | <label i18n for="first-step-channel">Channel</label> |
28 | <div class="peertube-select-container"> | 28 | <my-select-channel |
29 | <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> | 29 | labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId" |
30 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 30 | ></my-select-channel> |
31 | </select> | ||
32 | </div> | ||
33 | </div> | 31 | </div> |
34 | 32 | ||
35 | <div class="form-group"> | 33 | <div class="form-group"> |
36 | <label i18n for="first-step-privacy">Privacy</label> | 34 | <label i18n for="first-step-privacy">Privacy</label> |
37 | <div class="peertube-select-container"> | 35 | <my-select-options |
38 | <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> | 36 | labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId" |
39 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | 37 | ></my-select-options> |
40 | </select> | ||
41 | </div> | ||
42 | </div> | 38 | </div> |
43 | 39 | ||
44 | <input | 40 | <input |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html index 2e434271e..9b5cc3361 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.html | |||
@@ -20,20 +20,16 @@ | |||
20 | 20 | ||
21 | <div class="form-group"> | 21 | <div class="form-group"> |
22 | <label i18n for="first-step-channel">Channel</label> | 22 | <label i18n for="first-step-channel">Channel</label> |
23 | <div class="peertube-select-container"> | 23 | <my-select-channel |
24 | <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> | 24 | labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId" |
25 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 25 | ></my-select-channel> |
26 | </select> | ||
27 | </div> | ||
28 | </div> | 26 | </div> |
29 | 27 | ||
30 | <div class="form-group"> | 28 | <div class="form-group"> |
31 | <label i18n for="first-step-privacy">Privacy</label> | 29 | <label i18n for="first-step-privacy">Privacy</label> |
32 | <div class="peertube-select-container"> | 30 | <my-select-options |
33 | <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> | 31 | labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId" |
34 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | 32 | ></my-select-options> |
35 | </select> | ||
36 | </div> | ||
37 | </div> | 33 | </div> |
38 | 34 | ||
39 | <input | 35 | <input |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.scss b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss index cdb7c8280..17c5f63e9 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-send.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.scss | |||
@@ -26,6 +26,13 @@ $width-size: 190px; | |||
26 | .peertube-select-container { | 26 | .peertube-select-container { |
27 | @include peertube-select-container($width-size); | 27 | @include peertube-select-container($width-size); |
28 | } | 28 | } |
29 | my-select-options ::ng-deep ng-select, | ||
30 | my-select-channel ::ng-deep ng-select { | ||
31 | width: $width-size; | ||
32 | @media screen and (max-width: $width-size) { | ||
33 | width: 100%; | ||
34 | } | ||
35 | } | ||
29 | 36 | ||
30 | input[type=text] { | 37 | input[type=text] { |
31 | @include peertube-input-text($width-size); | 38 | @include peertube-input-text($width-size); |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts index 2e658dfae..86f2b376f 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-send.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-send.ts | |||
@@ -10,7 +10,7 @@ import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' | |||
10 | @Directive() | 10 | @Directive() |
11 | // tslint:disable-next-line: directive-class-suffix | 11 | // tslint:disable-next-line: directive-class-suffix |
12 | export abstract class VideoSend extends FormReactive implements OnInit { | 12 | export abstract class VideoSend extends FormReactive implements OnInit { |
13 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 13 | userVideoChannels: { id: number, label: string, support: string, avatarPath?: string }[] = [] |
14 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 14 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] |
15 | videoCaptions: VideoCaptionEdit[] = [] | 15 | videoCaptions: VideoCaptionEdit[] = [] |
16 | 16 | ||
@@ -44,7 +44,7 @@ export abstract class VideoSend extends FormReactive implements OnInit { | |||
44 | this.serverService.getVideoPrivacies() | 44 | this.serverService.getVideoPrivacies() |
45 | .subscribe( | 45 | .subscribe( |
46 | privacies => { | 46 | privacies => { |
47 | this.videoPrivacies = privacies | 47 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies) |
48 | 48 | ||
49 | this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY | 49 | this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY |
50 | }) | 50 | }) |
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 95b6628f6..ed697c25b 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -9,21 +9,16 @@ | |||
9 | 9 | ||
10 | <div class="form-group form-group-channel"> | 10 | <div class="form-group form-group-channel"> |
11 | <label i18n for="first-step-channel">Channel</label> | 11 | <label i18n for="first-step-channel">Channel</label> |
12 | <div class="peertube-select-container"> | 12 | <my-select-channel |
13 | <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control"> | 13 | labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId" |
14 | <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option> | 14 | ></my-select-channel> |
15 | </select> | ||
16 | </div> | ||
17 | </div> | 15 | </div> |
18 | 16 | ||
19 | <div class="form-group"> | 17 | <div class="form-group"> |
20 | <label i18n for="first-step-privacy">Privacy</label> | 18 | <label i18n for="first-step-privacy">Privacy</label> |
21 | <div class="peertube-select-container"> | 19 | <my-select-options |
22 | <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control"> | 20 | labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId" |
23 | <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option> | 21 | ></my-select-options> |
24 | <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option> | ||
25 | </select> | ||
26 | </div> | ||
27 | </div> | 22 | </div> |
28 | 23 | ||
29 | <ng-container *ngIf="isUploadingAudioFile"> | 24 | <ng-container *ngIf="isUploadingAudioFile"> |
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index 581199d65..de4f65df3 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts | |||
@@ -17,7 +17,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
17 | video: VideoEdit | 17 | video: VideoEdit |
18 | 18 | ||
19 | isUpdatingVideo = false | 19 | isUpdatingVideo = false |
20 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 20 | userVideoChannels: { id: number, label: string, support: string, avatar?: string }[] = [] |
21 | schedulePublicationPossible = false | 21 | schedulePublicationPossible = false |
22 | videoCaptions: VideoCaptionEdit[] = [] | 22 | videoCaptions: VideoCaptionEdit[] = [] |
23 | waitTranscodingEnabled = true | 23 | waitTranscodingEnabled = true |
diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 30bcf4d74..a391913d8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -28,7 +28,12 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
28 | .listAccountVideoChannels(video.account) | 28 | .listAccountVideoChannels(video.account) |
29 | .pipe( | 29 | .pipe( |
30 | map(result => result.data), | 30 | map(result => result.data), |
31 | map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support }))) | 31 | map(videoChannels => videoChannels.map(c => ({ |
32 | id: c.id, | ||
33 | label: c.displayName, | ||
34 | support: c.support, | ||
35 | avatarPath: c.avatar?.path | ||
36 | }))) | ||
32 | ), | 37 | ), |
33 | 38 | ||
34 | this.videoCaptionService | 39 | this.videoCaptionService |
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index 825b6ca96..aa37fdd46 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts | |||
@@ -16,7 +16,10 @@ function getParameterByName (name: string, url: string) { | |||
16 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) | 16 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) |
17 | } | 17 | } |
18 | 18 | ||
19 | function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) { | 19 | function populateAsyncUserVideoChannels ( |
20 | authService: AuthService, | ||
21 | channel: { id: number, label: string, support?: string, avatarPath?: string, recent?: boolean }[] | ||
22 | ) { | ||
20 | return new Promise(res => { | 23 | return new Promise(res => { |
21 | authService.userInformationLoaded | 24 | authService.userInformationLoaded |
22 | .subscribe( | 25 | .subscribe( |
@@ -27,7 +30,12 @@ function populateAsyncUserVideoChannels (authService: AuthService, channel: { id | |||
27 | const videoChannels = user.videoChannels | 30 | const videoChannels = user.videoChannels |
28 | if (Array.isArray(videoChannels) === false) return | 31 | if (Array.isArray(videoChannels) === false) return |
29 | 32 | ||
30 | videoChannels.forEach(c => channel.push({ id: c.id, label: c.displayName, support: c.support })) | 33 | videoChannels.forEach(c => channel.push({ |
34 | id: c.id, | ||
35 | label: c.displayName, | ||
36 | support: c.support, | ||
37 | avatarPath: c.avatar?.path | ||
38 | })) | ||
31 | 39 | ||
32 | return res() | 40 | return res() |
33 | } | 41 | } |
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 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | 1 | import { I18n } from '@ngx-translate/i18n-polyfill' |
2 | import { Validators } from '@angular/forms' | 2 | import { Validators, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { BuildFormValidator } from './form-validator.service' | 4 | import { BuildFormValidator } from './form-validator.service' |
5 | 5 | ||
@@ -13,7 +13,8 @@ export class VideoValidatorsService { | |||
13 | readonly VIDEO_IMAGE: BuildFormValidator | 13 | readonly VIDEO_IMAGE: BuildFormValidator |
14 | readonly VIDEO_CHANNEL: BuildFormValidator | 14 | readonly VIDEO_CHANNEL: BuildFormValidator |
15 | readonly VIDEO_DESCRIPTION: BuildFormValidator | 15 | readonly VIDEO_DESCRIPTION: BuildFormValidator |
16 | readonly VIDEO_TAGS: BuildFormValidator | 16 | readonly VIDEO_TAGS_ARRAY: BuildFormValidator |
17 | readonly VIDEO_TAG: BuildFormValidator | ||
17 | readonly VIDEO_SUPPORT: BuildFormValidator | 18 | readonly VIDEO_SUPPORT: BuildFormValidator |
18 | readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator | 19 | readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator |
19 | readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator | 20 | readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator |
@@ -71,7 +72,7 @@ export class VideoValidatorsService { | |||
71 | } | 72 | } |
72 | } | 73 | } |
73 | 74 | ||
74 | this.VIDEO_TAGS = { | 75 | this.VIDEO_TAG = { |
75 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ], | 76 | VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ], |
76 | MESSAGES: { | 77 | MESSAGES: { |
77 | 'minlength': this.i18n('A tag should be more than 2 characters long.'), | 78 | 'minlength': this.i18n('A tag should be more than 2 characters long.'), |
@@ -79,6 +80,14 @@ export class VideoValidatorsService { | |||
79 | } | 80 | } |
80 | } | 81 | } |
81 | 82 | ||
83 | this.VIDEO_TAGS_ARRAY = { | ||
84 | VALIDATORS: [ Validators.maxLength(5), this.arrayTagLengthValidator() ], | ||
85 | MESSAGES: { | ||
86 | 'maxlength': this.i18n('A maximum of 5 tags can be used on a video.'), | ||
87 | 'arrayTagLength': this.i18n('A tag should be more than 2, and less than 30 characters long.') | ||
88 | } | ||
89 | } | ||
90 | |||
82 | this.VIDEO_SUPPORT = { | 91 | this.VIDEO_SUPPORT = { |
83 | VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ], | 92 | VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ], |
84 | MESSAGES: { | 93 | MESSAGES: { |
@@ -99,4 +108,16 @@ export class VideoValidatorsService { | |||
99 | MESSAGES: {} | 108 | MESSAGES: {} |
100 | } | 109 | } |
101 | } | 110 | } |
111 | |||
112 | arrayTagLengthValidator (min = 2, max = 30): ValidatorFn { | ||
113 | return (control: AbstractControl): ValidationErrors => { | ||
114 | const array = control.value as Array<string> | ||
115 | |||
116 | if (array.every(e => e.length > min && e.length < max)) { | ||
117 | return null | ||
118 | } | ||
119 | |||
120 | return { 'arrayTagLength': true } | ||
121 | } | ||
122 | } | ||
102 | } | 123 | } |
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 @@ | |||
1 | <ng-select | ||
2 | [(ngModel)]="selectedId" | ||
3 | (ngModelChange)="onModelChange()" | ||
4 | [bindLabel]="bindLabel" | ||
5 | [bindValue]="bindValue" | ||
6 | [clearable]="clearable" | ||
7 | [searchable]="searchable" | ||
8 | > | ||
9 | <ng-option *ngFor="let channel of channels" [value]="channel.id"> | ||
10 | <img | ||
11 | class="avatar mr-1" | ||
12 | [src]="channel.avatarPath" | ||
13 | /> | ||
14 | {{ channel.label }} | ||
15 | </ng-option> | ||
16 | </ng-select> | ||
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 @@ | |||
1 | import { Component, Input, forwardRef, ViewChild } from '@angular/core' | ||
2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' | ||
3 | import { Actor } from '../shared-main' | ||
4 | |||
5 | @Component({ | ||
6 | selector: 'my-select-channel', | ||
7 | styleUrls: [ './select-shared.component.scss' ], | ||
8 | templateUrl: './select-channel.component.html', | ||
9 | providers: [ | ||
10 | { | ||
11 | provide: NG_VALUE_ACCESSOR, | ||
12 | useExisting: forwardRef(() => SelectChannelComponent), | ||
13 | multi: true | ||
14 | } | ||
15 | ] | ||
16 | }) | ||
17 | export class SelectChannelComponent implements ControlValueAccessor { | ||
18 | @Input() items: { id: number, label: string, support: string, avatarPath?: string }[] = [] | ||
19 | |||
20 | selectedId: number | ||
21 | |||
22 | // ng-select options | ||
23 | bindLabel = 'label' | ||
24 | bindValue = 'id' | ||
25 | clearable = false | ||
26 | searchable = false | ||
27 | |||
28 | get channels () { | ||
29 | return this.items.map(c => Object.assign(c, { | ||
30 | avatarPath: c.avatarPath ? c.avatarPath : Actor.GET_DEFAULT_AVATAR_URL() | ||
31 | })) | ||
32 | } | ||
33 | |||
34 | propagateChange = (_: any) => { /* empty */ } | ||
35 | |||
36 | writeValue (id: number) { | ||
37 | this.selectedId = id | ||
38 | } | ||
39 | |||
40 | registerOnChange (fn: (_: any) => void) { | ||
41 | this.propagateChange = fn | ||
42 | } | ||
43 | |||
44 | registerOnTouched () { | ||
45 | // Unused | ||
46 | } | ||
47 | |||
48 | onModelChange () { | ||
49 | this.propagateChange(this.selectedId) | ||
50 | } | ||
51 | } | ||
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 @@ | |||
1 | <ng-select | ||
2 | [items]="items" | ||
3 | [groupBy]="groupBy" | ||
4 | [(ngModel)]="selectedId" | ||
5 | (ngModelChange)="onModelChange()" | ||
6 | [bindLabel]="bindLabel" | ||
7 | [bindValue]="bindValue" | ||
8 | [clearable]="clearable" | ||
9 | [searchable]="searchable" | ||
10 | > | ||
11 | <ng-template ng-option-tmp let-item="item" let-index="index"> | ||
12 | {{ item.label }} | ||
13 | <ng-container *ngIf="item.description"> | ||
14 | <br> | ||
15 | <span [title]="item.description" class="text-muted">{{ item.description }}</span> | ||
16 | </ng-container> | ||
17 | </ng-template> | ||
18 | </ng-select> | ||
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 @@ | |||
1 | import { Component, Input, forwardRef } from '@angular/core' | ||
2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' | ||
3 | |||
4 | export type SelectOptionsItem = { id: number | string, label: string, description?: string } | ||
5 | |||
6 | @Component({ | ||
7 | selector: 'my-select-options', | ||
8 | styleUrls: [ './select-shared.component.scss' ], | ||
9 | templateUrl: './select-options.component.html', | ||
10 | providers: [ | ||
11 | { | ||
12 | provide: NG_VALUE_ACCESSOR, | ||
13 | useExisting: forwardRef(() => SelectOptionsComponent), | ||
14 | multi: true | ||
15 | } | ||
16 | ] | ||
17 | }) | ||
18 | export class SelectOptionsComponent implements ControlValueAccessor { | ||
19 | @Input() items: SelectOptionsItem[] = [] | ||
20 | @Input() clearable = false | ||
21 | @Input() searchable = false | ||
22 | @Input() bindValue = 'id' | ||
23 | @Input() groupBy: string | ||
24 | |||
25 | selectedId: number | string | ||
26 | |||
27 | // ng-select options | ||
28 | bindLabel = 'label' | ||
29 | |||
30 | propagateChange = (_: any) => { /* empty */ } | ||
31 | |||
32 | writeValue (id: number | string) { | ||
33 | this.selectedId = id | ||
34 | } | ||
35 | |||
36 | registerOnChange (fn: (_: any) => void) { | ||
37 | this.propagateChange = fn | ||
38 | } | ||
39 | |||
40 | registerOnTouched () { | ||
41 | // Unused | ||
42 | } | ||
43 | |||
44 | onModelChange () { | ||
45 | this.propagateChange(this.selectedId) | ||
46 | } | ||
47 | } | ||
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 @@ | |||
1 | $width-size: auto; | ||
2 | |||
3 | ng-select { | ||
4 | width: $width-size; | ||
5 | @media screen and (max-width: $width-size) { | ||
6 | width: 100%; | ||
7 | } | ||
8 | } | ||
9 | |||
10 | // make sure the image is vertically adjusted | ||
11 | ng-select ::ng-deep .ng-value-label img { | ||
12 | position: relative; | ||
13 | top: -1px; | ||
14 | } | ||
15 | |||
16 | ng-select ::ng-deep img { | ||
17 | border-radius: 50%; | ||
18 | height: 20px; | ||
19 | width: 20px; | ||
20 | } | ||
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 @@ | |||
1 | <ng-select | ||
2 | [items]="items" | ||
3 | [(ngModel)]="_items" | ||
4 | (ngModelChange)="onModelChange()" | ||
5 | i18n-placeholder placeholder="Enter a new tag" | ||
6 | [maxSelectedItems]="5" | ||
7 | [clearable]="true" | ||
8 | [addTag]="true" | ||
9 | [multiple]="true" | ||
10 | [isOpen]="false" | ||
11 | [searchable]="true" | ||
12 | > | ||
13 | </ng-select> | ||
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 @@ | |||
1 | ng-select ::ng-deep .ng-arrow-wrapper { | ||
2 | display: none; | ||
3 | } | ||
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 @@ | |||
1 | import { Component, Input, forwardRef } from '@angular/core' | ||
2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-select-tags', | ||
6 | styleUrls: [ './select-shared.component.scss', './select-tags.component.scss' ], | ||
7 | templateUrl: './select-tags.component.html', | ||
8 | providers: [ | ||
9 | { | ||
10 | provide: NG_VALUE_ACCESSOR, | ||
11 | useExisting: forwardRef(() => SelectTagsComponent), | ||
12 | multi: true | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export class SelectTagsComponent implements ControlValueAccessor { | ||
17 | @Input() items: string[] = [] | ||
18 | @Input() _items: string[] = [] | ||
19 | |||
20 | propagateChange = (_: any) => { /* empty */ } | ||
21 | |||
22 | writeValue (items: string[]) { | ||
23 | this._items = items | ||
24 | this.propagateChange(this._items) | ||
25 | } | ||
26 | |||
27 | registerOnChange (fn: (_: any) => void) { | ||
28 | this.propagateChange = fn | ||
29 | } | ||
30 | |||
31 | registerOnTouched () { | ||
32 | // Unused | ||
33 | } | ||
34 | |||
35 | onModelChange () { | ||
36 | this.propagateChange(this._items) | ||
37 | } | ||
38 | } | ||
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' | |||
28 | import { ReactiveFileComponent } from './reactive-file.component' | 28 | import { ReactiveFileComponent } from './reactive-file.component' |
29 | import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' | 29 | import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' |
30 | import { TimestampInputComponent } from './timestamp-input.component' | 30 | import { TimestampInputComponent } from './timestamp-input.component' |
31 | import { SelectChannelComponent } from './select-channel.component' | ||
32 | import { SelectOptionsComponent } from './select-options.component' | ||
33 | import { SelectTagsComponent } from './select-tags.component' | ||
31 | 34 | ||
32 | @NgModule({ | 35 | @NgModule({ |
33 | imports: [ | 36 | imports: [ |
@@ -45,7 +48,10 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
45 | PreviewUploadComponent, | 48 | PreviewUploadComponent, |
46 | ReactiveFileComponent, | 49 | ReactiveFileComponent, |
47 | TextareaAutoResizeDirective, | 50 | TextareaAutoResizeDirective, |
48 | TimestampInputComponent | 51 | TimestampInputComponent, |
52 | SelectChannelComponent, | ||
53 | SelectOptionsComponent, | ||
54 | SelectTagsComponent | ||
49 | ], | 55 | ], |
50 | 56 | ||
51 | exports: [ | 57 | exports: [ |
@@ -58,7 +64,10 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
58 | PreviewUploadComponent, | 64 | PreviewUploadComponent, |
59 | ReactiveFileComponent, | 65 | ReactiveFileComponent, |
60 | TextareaAutoResizeDirective, | 66 | TextareaAutoResizeDirective, |
61 | TimestampInputComponent | 67 | TimestampInputComponent, |
68 | SelectChannelComponent, | ||
69 | SelectOptionsComponent, | ||
70 | SelectTagsComponent | ||
62 | ], | 71 | ], |
63 | 72 | ||
64 | providers: [ | 73 | 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 { | |||
17 | NgbPopoverModule, | 17 | NgbPopoverModule, |
18 | NgbTooltipModule | 18 | NgbTooltipModule |
19 | } from '@ng-bootstrap/ng-bootstrap' | 19 | } from '@ng-bootstrap/ng-bootstrap' |
20 | import { NgSelectModule } from '@ng-select/ng-select' | ||
20 | import { I18n } from '@ngx-translate/i18n-polyfill' | 21 | import { I18n } from '@ngx-translate/i18n-polyfill' |
21 | import { SharedGlobalIconModule } from '../shared-icons' | 22 | import { SharedGlobalIconModule } from '../shared-icons' |
22 | import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account' | 23 | import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account' |
@@ -55,6 +56,8 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | |||
55 | MultiSelectModule, | 56 | MultiSelectModule, |
56 | InputSwitchModule, | 57 | InputSwitchModule, |
57 | 58 | ||
59 | NgSelectModule, | ||
60 | |||
58 | SharedGlobalIconModule | 61 | SharedGlobalIconModule |
59 | ], | 62 | ], |
60 | 63 | ||
@@ -134,7 +137,9 @@ import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | |||
134 | TopMenuDropdownComponent, | 137 | TopMenuDropdownComponent, |
135 | 138 | ||
136 | UserQuotaComponent, | 139 | UserQuotaComponent, |
137 | UserNotificationsComponent | 140 | UserNotificationsComponent, |
141 | |||
142 | NgSelectModule | ||
138 | ], | 143 | ], |
139 | 144 | ||
140 | providers: [ | 145 | 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 { | |||
339 | const base = [ | 339 | const base = [ |
340 | { | 340 | { |
341 | id: VideoPrivacy.PRIVATE, | 341 | id: VideoPrivacy.PRIVATE, |
342 | label: this.i18n('Only I can see this video') | 342 | description: this.i18n('Only I can see this video') |
343 | }, | 343 | }, |
344 | { | 344 | { |
345 | id: VideoPrivacy.UNLISTED, | 345 | id: VideoPrivacy.UNLISTED, |
346 | label: this.i18n('Only people with the private link can see this video') | 346 | description: this.i18n('Only shareable via a private link') |
347 | }, | 347 | }, |
348 | { | 348 | { |
349 | id: VideoPrivacy.PUBLIC, | 349 | id: VideoPrivacy.PUBLIC, |
350 | label: this.i18n('Anyone can see this video') | 350 | description: this.i18n('Anyone can see this video') |
351 | }, | 351 | }, |
352 | { | 352 | { |
353 | id: VideoPrivacy.INTERNAL, | 353 | id: VideoPrivacy.INTERNAL, |
354 | label: this.i18n('Only users of this instance can see this video') | 354 | description: this.i18n('Only users of this instance can see this video') |
355 | } | 355 | } |
356 | ] | 356 | ] |
357 | 357 | ||
358 | return base.filter(o => !!privacies.find(p => p.id === o.id)) | 358 | return base |
359 | .filter(o => !!privacies.find(p => p.id === o.id)) // filter down to privacies that where in the input | ||
360 | .map(o => ({ ...privacies[o.id - 1], ...o })) // merge the input privacies that contain a label, and extend them with a description | ||
359 | } | 361 | } |
360 | 362 | ||
361 | nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { | 363 | nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { |