aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-08-11 16:07:53 +0200
committerChocobozzz <me@florianbigard.com>2020-08-11 16:18:42 +0200
commit52c4976fcf4ee255a3af68ff9778e4f5c4f84bd4 (patch)
tree887d2b6106548ad23cf016d82baf1977198027d9 /client
parent3d25d5de33d8aa0ba00d7514522b25d22bf0e0a1 (diff)
downloadPeerTube-52c4976fcf4ee255a3af68ff9778e4f5c4f84bd4.tar.gz
PeerTube-52c4976fcf4ee255a3af68ff9778e4f5c4f84bd4.tar.zst
PeerTube-52c4976fcf4ee255a3af68ff9778e4f5c4f84bd4.zip
Use ng select for multiselect
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html22
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts33
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html44
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss1
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts34
-rw-r--r--client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts3
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-send.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts3
-rw-r--r--client/src/app/helpers/utils.ts2
-rw-r--r--client/src/app/shared/shared-forms/index.ts1
-rw-r--r--client/src/app/shared/shared-forms/select-shared.component.scss20
-rw-r--r--client/src/app/shared/shared-forms/select/index.ts4
-rw-r--r--client/src/app/shared/shared-forms/select/select-channel.component.html (renamed from client/src/app/shared/shared-forms/select-channel.component.html)0
-rw-r--r--client/src/app/shared/shared-forms/select/select-channel.component.ts (renamed from client/src/app/shared/shared-forms/select-channel.component.ts)2
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.html41
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.scss18
-rw-r--r--client/src/app/shared/shared-forms/select/select-checkbox.component.ts75
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.html (renamed from client/src/app/shared/shared-forms/select-options.component.html)5
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts (renamed from client/src/app/shared/shared-forms/select-options.component.ts)12
-rw-r--r--client/src/app/shared/shared-forms/select/select-shared.component.scss32
-rw-r--r--client/src/app/shared/shared-forms/select/select-tags.component.html (renamed from client/src/app/shared/shared-forms/select-tags.component.html)0
-rw-r--r--client/src/app/shared/shared-forms/select/select-tags.component.scss (renamed from client/src/app/shared/shared-forms/select-tags.component.scss)0
-rw-r--r--client/src/app/shared/shared-forms/select/select-tags.component.ts (renamed from client/src/app/shared/shared-forms/select-tags.component.ts)2
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts15
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.html10
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.scss4
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.ts45
-rw-r--r--client/src/sass/include/_mixins.scss11
-rw-r--r--client/src/sass/primeng-custom.scss61
31 files changed, 314 insertions, 196 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 8fdced1c7..b82281c6c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -48,11 +48,12 @@
48 <label i18n for="instanceCategories">Main instance categories</label> 48 <label i18n for="instanceCategories">Main instance categories</label>
49 49
50 <div> 50 <div>
51 <p-multiSelect 51 <my-select-checkbox
52 inputId="instanceCategories" [options]="categoryItems" formControlName="categories" [showToggleAll]="false" 52 id="instanceCategories"
53 [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()" 53 formControlName="categories" [availableItems]="categoryItems"
54 emptyFilterMessage="No results found" i18n-emptyFilterMessage 54 [selectableGroup]="false"
55 ></p-multiSelect> 55 >
56 </my-select-checkbox>
56 </div> 57 </div>
57 </div> 58 </div>
58 59
@@ -60,11 +61,12 @@
60 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label> 61 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
61 62
62 <div> 63 <div>
63 <p-multiSelect 64 <my-select-checkbox
64 inputId="instanceLanguages" [options]="languageItems" formControlName="languages" [showToggleAll]="false" 65 id="instanceLanguages"
65 [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()" 66 formControlName="languages" [availableItems]="languageItems"
66 emptyFilterMessage="No results found" i18n-emptyFilterMessage 67 [selectableGroup]="false"
67 ></p-multiSelect> 68 >
69 </my-select-checkbox>
68 </div> 70 </div>
69 </div> 71 </div>
70 72
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index 9618100b5..f8f2d5fdc 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -30,6 +30,10 @@ input[type=checkbox] {
30 @include peertube-select-container($form-base-input-width); 30 @include peertube-select-container($form-base-input-width);
31} 31}
32 32
33my-select-checkbox {
34 @include ng-select($form-base-input-width);
35}
36
33input[type=submit] { 37input[type=submit] {
34 @include peertube-button; 38 @include peertube-button;
35 @include orange-button; 39 @include orange-button;
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 69629770f..00a0bfad2 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -1,11 +1,16 @@
1import { SelectItem } from 'primeng/api'
2import { forkJoin } from 'rxjs' 1import { forkJoin } from 'rxjs'
3import { ViewportScroller } from '@angular/common' 2import { ViewportScroller } from '@angular/common'
4import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core' 3import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6import { Notifier } from '@app/core' 5import { Notifier } from '@app/core'
7import { ServerService } from '@app/core/server/server.service' 6import { ServerService } from '@app/core/server/server.service'
8import { CustomConfigValidatorsService, FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' 7import {
8 CustomConfigValidatorsService,
9 FormReactive,
10 FormValidatorService,
11 SelectOptionsItem,
12 UserValidatorsService
13} from '@app/shared/shared-forms'
9import { NgbNav } from '@ng-bootstrap/ng-bootstrap' 14import { NgbNav } from '@ng-bootstrap/ng-bootstrap'
10import { I18n } from '@ngx-translate/i18n-polyfill' 15import { I18n } from '@ngx-translate/i18n-polyfill'
11import { CustomConfig, ServerConfig } from '@shared/models' 16import { CustomConfig, ServerConfig } from '@shared/models'
@@ -25,8 +30,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
25 resolutions: { id: string, label: string, description?: string }[] = [] 30 resolutions: { id: string, label: string, description?: string }[] = []
26 transcodingThreadOptions: { label: string, value: number }[] = [] 31 transcodingThreadOptions: { label: string, value: number }[] = []
27 32
28 languageItems: SelectItem[] = [] 33 languageItems: SelectOptionsItem[] = []
29 categoryItems: SelectItem[] = [] 34 categoryItems: SelectOptionsItem[] = []
30 35
31 private serverConfig: ServerConfig 36 private serverConfig: ServerConfig
32 37
@@ -290,22 +295,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
290 ) 295 )
291 } 296 }
292 297
293 getSelectedLanguageLabel () {
294 return this.i18n('{{\'{0} languages selected')
295 }
296
297 getDefaultLanguageLabel () {
298 return this.i18n('No language')
299 }
300
301 getSelectedCategoryLabel () {
302 return this.i18n('{{\'{0} categories selected')
303 }
304
305 getDefaultCategoryLabel () {
306 return this.i18n('No category')
307 }
308
309 gotoAnchor () { 298 gotoAnchor () {
310 const hashToNav = { 299 const hashToNav = {
311 'customizations': 'advanced-configuration' 300 'customizations': 'advanced-configuration'
@@ -331,8 +320,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
331 ([ config, languages, categories ]) => { 320 ([ config, languages, categories ]) => {
332 this.customConfig = config 321 this.customConfig = config
333 322
334 this.languageItems = languages.map(l => ({ label: l.label, value: l.id })) 323 this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
335 this.categoryItems = categories.map(l => ({ label: l.label, value: l.id })) 324 this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
336 325
337 this.updateForm() 326 this.updateForm()
338 // Force form validation 327 // Force form validation
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 3090247e5..d02f6526f 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -57,12 +57,12 @@
57 <div role="menu" class="dropdown-menu" ngbDropdownMenu> 57 <div role="menu" class="dropdown-menu" ngbDropdownMenu>
58 <div class="dropdown-header" i18n>Table parameters</div> 58 <div class="dropdown-header" i18n>Table parameters</div>
59 <div ngbDropdownItem class="dropdown-item"> 59 <div ngbDropdownItem class="dropdown-item">
60 <p-multiSelect 60 <my-select-checkbox
61 [options]="columns" [showToggleAll]="true" [(ngModel)]="selectedColumns" optionLabel="label" 61 name="columns"
62 emptyFilterMessage="No matching column found" i18n-emptyFilterMessage [filter]="false" 62 [availableItems]="columns"
63 selectedItemsLabel="{0} columns displayed" i18n-emptyFilterMessage [showHeader]="false" 63 [selectableGroup]="false" [(ngModel)]="selectedColumns"
64 [maxSelectedLabels]="4" 64 >
65 ></p-multiSelect> 65 </my-select-checkbox>
66 </div> 66 </div>
67 <div ngbDropdownItem class="dropdown-item"> 67 <div ngbDropdownItem class="dropdown-item">
68 <my-peertube-checkbox inputName="highlightBannedUsers" [(ngModel)]="highlightBannedUsers" 68 <my-peertube-checkbox inputName="highlightBannedUsers" [(ngModel)]="highlightBannedUsers"
@@ -71,14 +71,14 @@
71 </div> 71 </div>
72 </div> 72 </div>
73 </th> 73 </th>
74 <th *ngIf="getColumn('username')" pResizableColumn i18n pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th> 74 <th *ngIf="isSelected('username')" pResizableColumn i18n pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th>
75 <th *ngIf="getColumn('email')" i18n>{{ getColumn('email').label }}</th> 75 <th *ngIf="isSelected('email')" i18n>{{ getColumn('email').label }}</th>
76 <th *ngIf="getColumn('quota')" style="width: 160px;" i18n pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th> 76 <th *ngIf="isSelected('quota')" style="width: 160px;" i18n pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th>
77 <th *ngIf="getColumn('quotaDaily')" style="width: 160px;" i18n>{{ getColumn('quotaDaily').label }}</th> 77 <th *ngIf="isSelected('quotaDaily')" style="width: 160px;" i18n>{{ getColumn('quotaDaily').label }}</th>
78 <th *ngIf="getColumn('role')" style="width: 120px;" i18n pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th> 78 <th *ngIf="isSelected('role')" style="width: 120px;" i18n pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th>
79 <th *ngIf="getColumn('pluginAuth')" style="width: 140px;" pResizableColumn i18n>{{ getColumn('pluginAuth').label }}</th> 79 <th *ngIf="isSelected('pluginAuth')" style="width: 140px;" pResizableColumn i18n>{{ getColumn('pluginAuth').label }}</th>
80 <th *ngIf="getColumn('createdAt')" style="width: 150px;" i18n pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th> 80 <th *ngIf="isSelected('createdAt')" style="width: 150px;" i18n pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th>
81 <th *ngIf="getColumn('lastLoginDate')" style="width: 150px;" i18n pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th> 81 <th *ngIf="isSelected('lastLoginDate')" style="width: 150px;" i18n pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th>
82 </tr> 82 </tr>
83 </ng-template> 83 </ng-template>
84 84
@@ -101,7 +101,7 @@
101 </my-user-moderation-dropdown> 101 </my-user-moderation-dropdown>
102 </td> 102 </td>
103 103
104 <td *ngIf="getColumn('username')"> 104 <td *ngIf="isSelected('username')">
105 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> 105 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
106 <div class="chip two-lines"> 106 <div class="chip two-lines">
107 <img 107 <img
@@ -118,7 +118,7 @@
118 </a> 118 </a>
119 </td> 119 </td>
120 120
121 <td *ngIf="getColumn('email')" [title]="user.email"> 121 <td *ngIf="isSelected('email')" [title]="user.email">
122 <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> 122 <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">
123 <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> 123 <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
124 </ng-container> 124 </ng-container>
@@ -135,7 +135,7 @@
135 </ng-template> 135 </ng-template>
136 </ng-template> 136 </ng-template>
137 137
138 <td *ngIf="getColumn('quota')"> 138 <td *ngIf="isSelected('quota')">
139 <div class="progress" i18n-title title="Total video quota"> 139 <div class="progress" i18n-title title="Total video quota">
140 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" 140 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
141 [attr.aria-valuenow]="user.rawVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuota"> 141 [attr.aria-valuenow]="user.rawVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuota">
@@ -145,7 +145,7 @@
145 </div> 145 </div>
146 </td> 146 </td>
147 147
148 <td *ngIf="getColumn('quotaDaily')"> 148 <td *ngIf="isSelected('quotaDaily')">
149 <div class="progress" i18n-title title="Total daily video quota"> 149 <div class="progress" i18n-title title="Total daily video quota">
150 <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }" 150 <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }"
151 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily"> 151 [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily">
@@ -155,18 +155,18 @@
155 </div> 155 </div>
156 </td> 156 </td>
157 157
158 <td *ngIf="getColumn('role')"> 158 <td *ngIf="isSelected('role')">
159 <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> 159 <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span>
160 <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> 160 <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span>
161 </td> 161 </td>
162 162
163 <td *ngIf="getColumn('pluginAuth')"> 163 <td *ngIf="isSelected('pluginAuth')">
164 <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container> 164 <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container>
165 </td> 165 </td>
166 166
167 <td *ngIf="getColumn('createdAt')" [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td> 167 <td *ngIf="isSelected('createdAt')" [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td>
168 168
169 <td *ngIf="getColumn('lastLoginDate')" [title]="user.lastLoginDate">{{ user.lastLoginDate | date: 'short' }}</td> 169 <td *ngIf="isSelected('lastLoginDate')" [title]="user.lastLoginDate">{{ user.lastLoginDate | date: 'short' }}</td>
170 </tr> 170 </tr>
171 </ng-template> 171 </ng-template>
172 172
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index 98fc4d027..50080bad6 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -61,6 +61,7 @@ my-global-icon {
61 61
62.input-group { 62.input-group {
63 @include peertube-input-group(300px); 63 @include peertube-input-group(300px);
64
64 input { 65 input {
65 flex: 1; 66 flex: 1;
66 } 67 }
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 699b2a6da..69d4e917d 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -30,9 +30,9 @@ export class UserListComponent extends RestTable implements OnInit {
30 30
31 selectedUsers: User[] = [] 31 selectedUsers: User[] = []
32 bulkUserActions: DropdownAction<User[]>[][] = [] 32 bulkUserActions: DropdownAction<User[]>[][] = []
33 columns: { key: string, label: string }[] 33 columns: { id: string, label: string }[]
34 34
35 private _selectedColumns: { key: string, label: string }[] 35 private _selectedColumns: string[]
36 private serverConfig: ServerConfig 36 private serverConfig: ServerConfig
37 37
38 constructor ( 38 constructor (
@@ -60,7 +60,7 @@ export class UserListComponent extends RestTable implements OnInit {
60 return this._selectedColumns 60 return this._selectedColumns
61 } 61 }
62 62
63 set selectedColumns (val) { 63 set selectedColumns (val: string[]) {
64 this._selectedColumns = val 64 this._selectedColumns = val
65 } 65 }
66 66
@@ -112,16 +112,18 @@ export class UserListComponent extends RestTable implements OnInit {
112 ] 112 ]
113 113
114 this.columns = [ 114 this.columns = [
115 { key: 'username', label: 'Username' }, 115 { id: 'username', label: 'Username' },
116 { key: 'email', label: 'Email' }, 116 { id: 'email', label: 'Email' },
117 { key: 'quota', label: 'Video quota' }, 117 { id: 'quota', label: 'Video quota' },
118 { key: 'role', label: 'Role' }, 118 { id: 'role', label: 'Role' },
119 { key: 'createdAt', label: 'Created' } 119 { id: 'createdAt', label: 'Created' }
120 ] 120 ]
121 this.selectedColumns = [ ...this.columns ] // make a full copy of the array 121
122 this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) 122 this.selectedColumns = this.columns.map(c => c.id)
123 this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) 123
124 this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) 124 this.columns.push({ id: 'quotaDaily', label: 'Daily quota' })
125 this.columns.push({ id: 'pluginAuth', label: 'Auth plugin' })
126 this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
125 } 127 }
126 128
127 getIdentifier () { 129 getIdentifier () {
@@ -139,8 +141,12 @@ export class UserListComponent extends RestTable implements OnInit {
139 } 141 }
140 } 142 }
141 143
142 getColumn (key: string) { 144 isSelected (id: string) {
143 return this.selectedColumns.find((col: { key: string }) => col.key === key) 145 return this.selectedColumns.find(c => c === id)
146 }
147
148 getColumn (id: string) {
149 return this.columns.find(c => c.id === id)
144 } 150 }
145 151
146 getUserVideoQuotaPercentage (user: UserForList) { 152 getUserVideoQuotaPercentage (user: UserForList) {
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
index 94a0f6168..774d58c90 100644
--- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
+++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
@@ -1,5 +1,4 @@
1import { FormReactive } from '@app/shared/shared-forms' 1import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
2import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
3import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' 2import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
4import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' 3import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
5 4
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 48c5508f4..ba3b7c96a 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
@@ -4,8 +4,7 @@ import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular
4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' 4import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
5import { ServerService } from '@app/core' 5import { ServerService } from '@app/core'
6import { removeElementFromArray } from '@app/helpers' 6import { removeElementFromArray } from '@app/helpers'
7import { FormReactiveValidationMessages, FormValidatorService, VideoValidatorsService } from '@app/shared/shared-forms' 7import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem, VideoValidatorsService } from '@app/shared/shared-forms'
8import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
9import { InstanceService } from '@app/shared/shared-instance' 8import { InstanceService } from '@app/shared/shared-instance'
10import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 9import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
11import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
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 c55e5f923..812936d7a 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
@@ -2,8 +2,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators'
2import { Directive, EventEmitter, OnInit } from '@angular/core' 2import { Directive, EventEmitter, OnInit } from '@angular/core'
3import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' 3import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core'
4import { populateAsyncUserVideoChannels } from '@app/helpers' 4import { populateAsyncUserVideoChannels } from '@app/helpers'
5import { FormReactive } from '@app/shared/shared-forms' 5import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
6import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
7import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
9import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' 8import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
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 263c71f3b..abd08f05c 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -2,8 +2,7 @@ import { map, switchMap } from 'rxjs/operators'
2import { Component, HostListener, OnInit } from '@angular/core' 2import { Component, HostListener, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { Notifier } from '@app/core' 4import { Notifier } from '@app/core'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms'
6import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
7import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
9import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts
index b925e6d98..d05541ca9 100644
--- a/client/src/app/helpers/utils.ts
+++ b/client/src/app/helpers/utils.ts
@@ -1,7 +1,7 @@
1import { DatePipe } from '@angular/common' 1import { DatePipe } from '@angular/common'
2import { SelectChannelItem } from '@app/shared/shared-forms'
2import { environment } from '../../environments/environment' 3import { environment } from '../../environments/environment'
3import { AuthService } from '../core/auth' 4import { AuthService } from '../core/auth'
4import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
5 5
6// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 6// Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
7function getParameterByName (name: string, url: string) { 7function getParameterByName (name: string, url: string) {
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index aa0ee015a..747df65cf 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -1,5 +1,6 @@
1export * from './form-validators' 1export * from './form-validators'
2export * from './form-reactive' 2export * from './form-reactive'
3export * from './select'
3export * from './input-readonly-copy.component' 4export * from './input-readonly-copy.component'
4export * from './markdown-textarea.component' 5export * from './markdown-textarea.component'
5export * from './peertube-checkbox.component' 6export * from './peertube-checkbox.component'
diff --git a/client/src/app/shared/shared-forms/select-shared.component.scss b/client/src/app/shared/shared-forms/select-shared.component.scss
deleted file mode 100644
index 4f231d28a..000000000
--- a/client/src/app/shared/shared-forms/select-shared.component.scss
+++ /dev/null
@@ -1,20 +0,0 @@
1$width-size: auto;
2
3ng-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
11ng-select ::ng-deep .ng-value-label img {
12 position: relative;
13 top: -1px;
14}
15
16ng-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/index.ts b/client/src/app/shared/shared-forms/select/index.ts
new file mode 100644
index 000000000..33459b23b
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/index.ts
@@ -0,0 +1,4 @@
1export * from './select-channel.component'
2export * from './select-options.component'
3export * from './select-tags.component'
4export * from './select-checkbox.component'
diff --git a/client/src/app/shared/shared-forms/select-channel.component.html b/client/src/app/shared/shared-forms/select/select-channel.component.html
index 897d13ee7..897d13ee7 100644
--- a/client/src/app/shared/shared-forms/select-channel.component.html
+++ b/client/src/app/shared/shared-forms/select/select-channel.component.html
diff --git a/client/src/app/shared/shared-forms/select-channel.component.ts b/client/src/app/shared/shared-forms/select/select-channel.component.ts
index ef4192095..1b0db9b6f 100644
--- a/client/src/app/shared/shared-forms/select-channel.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-channel.component.ts
@@ -1,6 +1,6 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Actor } from '../shared-main' 3import { Actor } from '@app/shared/shared-main/account/actor.model'
4 4
5export type SelectChannelItem = { 5export type SelectChannelItem = {
6 id: number 6 id: number
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.html b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
new file mode 100644
index 000000000..3f81dd152
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.html
@@ -0,0 +1,41 @@
1<ng-select
2 [items]="availableItems"
3 [(ngModel)]="selectedItems"
4 (ngModelChange)="onModelChange()"
5 i18n-placeholder placeholder="Add a new language"
6 [clearable]="true"
7 [multiple]="true"
8 [searchable]="true"
9 [closeOnSelect]="false"
10
11 bindValue="id"
12 bindLabel="label"
13
14 notFoundText="No items found" i18n-notFoundText
15
16 [selectableGroup]="selectableGroup"
17 [selectableGroupAsModel]="selectableGroupAsModel"
18
19 groupBy="group"
20 [compareWith]="compareFn"
21
22 [maxSelectedItems]="maxSelectedItems"
23>
24
25 <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index">
26 <div class="form-group-checkbox">
27 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
28 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
29 <span>{{ item.group }}</span>
30 </div>
31 </ng-template>
32
33 <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index">
34 <div class="form-group-checkbox">
35 <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
36 <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
37 <span>{{ item.label }}</span>
38 </div>
39 </ng-template>
40
41</ng-select>
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.scss b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
new file mode 100644
index 000000000..145f6b26c
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.scss
@@ -0,0 +1,18 @@
1@import '_variables';
2@import '_mixins';
3
4ng-select ::ng-deep {
5 .ng-option {
6 display: flex;
7 align-items: center;
8 }
9
10 .form-group-checkbox {
11 display: flex;
12 align-items: center;
13
14 input {
15 @include peertube-checkbox(1px);
16 }
17 }
18}
diff --git a/client/src/app/shared/shared-forms/select/select-checkbox.component.ts b/client/src/app/shared/shared-forms/select/select-checkbox.component.ts
new file mode 100644
index 000000000..93653fef1
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-checkbox.component.ts
@@ -0,0 +1,75 @@
1import { Component, Input, forwardRef } from '@angular/core'
2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
3import { SelectOptionsItem } from './select-options.component'
4
5export type ItemSelectCheckboxValue = { id?: string | number, group?: string } | string
6
7@Component({
8 selector: 'my-select-checkbox',
9 styleUrls: [ './select-shared.component.scss', 'select-checkbox.component.scss' ],
10 templateUrl: './select-checkbox.component.html',
11 providers: [
12 {
13 provide: NG_VALUE_ACCESSOR,
14 useExisting: forwardRef(() => SelectCheckboxComponent),
15 multi: true
16 }
17 ]
18})
19export class SelectCheckboxComponent implements ControlValueAccessor {
20 @Input() availableItems: SelectOptionsItem[] = []
21 @Input() selectedItems: ItemSelectCheckboxValue[] = []
22 @Input() selectableGroup: boolean
23 @Input() selectableGroupAsModel: boolean
24 @Input() maxSelectedItems: number
25
26 propagateChange = (_: any) => { /* empty */ }
27
28 writeValue (items: ItemSelectCheckboxValue[]) {
29 if (Array.isArray(items)) {
30 this.selectedItems = items.map(i => {
31 if (typeof i === 'string' || typeof i === 'number') {
32 return i + ''
33 }
34
35 if (i.group) {
36 return { group: i.group }
37 }
38
39 return { id: i.id + '' }
40 })
41 } else {
42 this.selectedItems = items
43 }
44
45 this.propagateChange(this.selectedItems)
46 }
47
48 registerOnChange (fn: (_: any) => void) {
49 this.propagateChange = fn
50 }
51
52 registerOnTouched () {
53 // Unused
54 }
55
56 onModelChange () {
57 this.propagateChange(this.selectedItems)
58 }
59
60 compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
61 if (typeof selected === 'string') {
62 return item.id === selected
63 }
64
65 if (this.selectableGroup && item.group && selected.group) {
66 return item.group === selected.group
67 }
68
69 if (selected.id && item.id) {
70 return item.id === selected.id
71 }
72
73 return false
74 }
75}
diff --git a/client/src/app/shared/shared-forms/select-options.component.html b/client/src/app/shared/shared-forms/select/select-options.component.html
index fda0c2c56..48eca1cf5 100644
--- a/client/src/app/shared/shared-forms/select-options.component.html
+++ b/client/src/app/shared/shared-forms/select/select-options.component.html
@@ -3,10 +3,11 @@
3 [groupBy]="groupBy" 3 [groupBy]="groupBy"
4 [(ngModel)]="selectedId" 4 [(ngModel)]="selectedId"
5 (ngModelChange)="onModelChange()" 5 (ngModelChange)="onModelChange()"
6 [bindLabel]="bindLabel"
7 [bindValue]="bindValue"
8 [clearable]="clearable" 6 [clearable]="clearable"
9 [searchable]="searchable" 7 [searchable]="searchable"
8
9 bindLabel="label"
10 bindValue="id"
10> 11>
11 <ng-template ng-option-tmp let-item="item" let-index="index"> 12 <ng-template ng-option-tmp let-item="item" let-index="index">
12 {{ item.label }} 13 {{ item.label }}
diff --git a/client/src/app/shared/shared-forms/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 09f7df53b..3ba24c732 100644
--- a/client/src/app/shared/shared-forms/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -1,7 +1,13 @@
1import { Component, Input, forwardRef } from '@angular/core' 1import { Component, Input, forwardRef } from '@angular/core'
2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' 2import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
3 3
4export type SelectOptionsItem = { id: number | string, label: string, description?: string } 4export type SelectOptionsItem = {
5 id: string | number
6 label: string
7 description?: string
8 group?: string
9 groupLabel?: string
10}
5 11
6@Component({ 12@Component({
7 selector: 'my-select-options', 13 selector: 'my-select-options',
@@ -19,14 +25,10 @@ export class SelectOptionsComponent implements ControlValueAccessor {
19 @Input() items: SelectOptionsItem[] = [] 25 @Input() items: SelectOptionsItem[] = []
20 @Input() clearable = false 26 @Input() clearable = false
21 @Input() searchable = false 27 @Input() searchable = false
22 @Input() bindValue = 'id'
23 @Input() groupBy: string 28 @Input() groupBy: string
24 29
25 selectedId: number | string 30 selectedId: number | string
26 31
27 // ng-select options
28 bindLabel = 'label'
29
30 propagateChange = (_: any) => { /* empty */ } 32 propagateChange = (_: any) => { /* empty */ }
31 33
32 writeValue (id: number | string) { 34 writeValue (id: number | string) {
diff --git a/client/src/app/shared/shared-forms/select/select-shared.component.scss b/client/src/app/shared/shared-forms/select/select-shared.component.scss
new file mode 100644
index 000000000..0b4c6b784
--- /dev/null
+++ b/client/src/app/shared/shared-forms/select/select-shared.component.scss
@@ -0,0 +1,32 @@
1@import '_variables';
2@import '_mixins';
3
4$form-base-input-width: auto;
5
6ng-select {
7 width: $form-base-input-width;
8
9 @media screen and (max-width: $form-base-input-width) {
10 width: 100%;
11 }
12}
13
14ng-select ::ng-deep {
15 .ng-value-container {
16 max-height: 100px;
17 overflow-y: auto;
18 overflow-x: hidden;
19 }
20
21 // make sure the image is vertically adjusted
22 .ng-value-label img {
23 position: relative;
24 top: -1px;
25 }
26
27 img {
28 border-radius: 50%;
29 height: 20px;
30 width: 20px;
31 }
32}
diff --git a/client/src/app/shared/shared-forms/select-tags.component.html b/client/src/app/shared/shared-forms/select/select-tags.component.html
index e1cd50882..e1cd50882 100644
--- a/client/src/app/shared/shared-forms/select-tags.component.html
+++ b/client/src/app/shared/shared-forms/select/select-tags.component.html
diff --git a/client/src/app/shared/shared-forms/select-tags.component.scss b/client/src/app/shared/shared-forms/select/select-tags.component.scss
index ad76bc7ee..ad76bc7ee 100644
--- a/client/src/app/shared/shared-forms/select-tags.component.scss
+++ b/client/src/app/shared/shared-forms/select/select-tags.component.scss
diff --git a/client/src/app/shared/shared-forms/select-tags.component.ts b/client/src/app/shared/shared-forms/select/select-tags.component.ts
index a8a19d788..93d199037 100644
--- a/client/src/app/shared/shared-forms/select-tags.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-tags.component.ts
@@ -33,8 +33,6 @@ export class SelectTagsComponent implements ControlValueAccessor {
33 } 33 }
34 34
35 onModelChange () { 35 onModelChange () {
36 console.log(this.selectedItems)
37
38 this.propagateChange(this.selectedItems) 36 this.propagateChange(this.selectedItems)
39 } 37 }
40} 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 ea6270083..0e0ed5bab 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'
3import { FormsModule, ReactiveFormsModule } from '@angular/forms' 3import { FormsModule, ReactiveFormsModule } from '@angular/forms'
4import { InputMaskModule } from 'primeng/inputmask' 4import { InputMaskModule } from 'primeng/inputmask'
5import { InputSwitchModule } from 'primeng/inputswitch' 5import { InputSwitchModule } from 'primeng/inputswitch'
6import { MultiSelectModule } from 'primeng/multiselect'
7import { NgSelectModule } from '@ng-select/ng-select' 6import { NgSelectModule } from '@ng-select/ng-select'
8import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service' 7import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service'
9import { SharedGlobalIconModule } from '../shared-icons' 8import { SharedGlobalIconModule } from '../shared-icons'
@@ -32,9 +31,7 @@ import { PreviewUploadComponent } from './preview-upload.component'
32import { ReactiveFileComponent } from './reactive-file.component' 31import { ReactiveFileComponent } from './reactive-file.component'
33import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' 32import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
34import { TimestampInputComponent } from './timestamp-input.component' 33import { TimestampInputComponent } from './timestamp-input.component'
35import { SelectChannelComponent } from './select-channel.component' 34import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select'
36import { SelectOptionsComponent } from './select-options.component'
37import { SelectTagsComponent } from './select-tags.component'
38 35
39@NgModule({ 36@NgModule({
40 imports: [ 37 imports: [
@@ -43,7 +40,6 @@ import { SelectTagsComponent } from './select-tags.component'
43 40
44 InputMaskModule, 41 InputMaskModule,
45 InputSwitchModule, 42 InputSwitchModule,
46 MultiSelectModule,
47 NgSelectModule, 43 NgSelectModule,
48 44
49 SharedMainModule, 45 SharedMainModule,
@@ -58,9 +54,11 @@ import { SelectTagsComponent } from './select-tags.component'
58 ReactiveFileComponent, 54 ReactiveFileComponent,
59 TextareaAutoResizeDirective, 55 TextareaAutoResizeDirective,
60 TimestampInputComponent, 56 TimestampInputComponent,
57
61 SelectChannelComponent, 58 SelectChannelComponent,
62 SelectOptionsComponent, 59 SelectOptionsComponent,
63 SelectTagsComponent 60 SelectTagsComponent,
61 SelectCheckboxComponent
64 ], 62 ],
65 63
66 exports: [ 64 exports: [
@@ -69,7 +67,6 @@ import { SelectTagsComponent } from './select-tags.component'
69 67
70 InputMaskModule, 68 InputMaskModule,
71 InputSwitchModule, 69 InputSwitchModule,
72 MultiSelectModule,
73 NgSelectModule, 70 NgSelectModule,
74 71
75 InputReadonlyCopyComponent, 72 InputReadonlyCopyComponent,
@@ -79,9 +76,11 @@ import { SelectTagsComponent } from './select-tags.component'
79 ReactiveFileComponent, 76 ReactiveFileComponent,
80 TextareaAutoResizeDirective, 77 TextareaAutoResizeDirective,
81 TimestampInputComponent, 78 TimestampInputComponent,
79
82 SelectChannelComponent, 80 SelectChannelComponent,
83 SelectOptionsComponent, 81 SelectOptionsComponent,
84 SelectTagsComponent 82 SelectTagsComponent,
83 SelectCheckboxComponent
85 ], 84 ],
86 85
87 providers: [ 86 providers: [
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.html b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
index bb9f59070..655b49e18 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.html
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.html
@@ -30,11 +30,11 @@
30 </my-help> 30 </my-help>
31 31
32 <div> 32 <div>
33 <p-multiSelect 33 <my-select-checkbox
34 inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" [showToggleAll]="true" 34 formControlName="videoLanguages" [availableItems]="languageItems"
35 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" 35 [selectableGroup]="true" [selectableGroupAsModel]="true"
36 emptyFilterMessage="No results found" i18n-emptyFilterMessage 36 >
37 ></p-multiSelect> 37 </my-select-checkbox >
38 </div> 38 </div>
39 </div> 39 </div>
40 40
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
index 430250b87..d6a17703a 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.scss
@@ -19,6 +19,10 @@ input[type=submit] {
19 margin-bottom: 30px; 19 margin-bottom: 30px;
20} 20}
21 21
22my-select-checkbox {
23 @include ng-select(340px);
24}
25
22.form-group-select { 26.form-group-select {
23 margin-bottom: 30px; 27 margin-bottom: 30px;
24} 28}
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
index 4e4539936..eb340e24d 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
@@ -1,10 +1,9 @@
1import { pick } from 'lodash-es' 1import { pick } from 'lodash-es'
2import { SelectItem } from 'primeng/api'
3import { forkJoin, Subject, Subscription } from 'rxjs' 2import { forkJoin, Subject, Subscription } from 'rxjs'
4import { first } from 'rxjs/operators' 3import { first } from 'rxjs/operators'
5import { Component, Input, OnDestroy, OnInit } from '@angular/core' 4import { Component, Input, OnDestroy, OnInit } from '@angular/core'
6import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormValidatorService, ItemSelectCheckboxValue, SelectOptionsItem } from '@app/shared/shared-forms'
8import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
9import { UserUpdateMe } from '@shared/models' 8import { UserUpdateMe } from '@shared/models'
10import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 9import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
@@ -20,10 +19,12 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
20 @Input() notifyOnUpdate = true 19 @Input() notifyOnUpdate = true
21 @Input() userInformationLoaded: Subject<any> 20 @Input() userInformationLoaded: Subject<any>
22 21
23 languageItems: SelectItem[] = [] 22 languageItems: SelectOptionsItem[] = []
24 defaultNSFWPolicy: NSFWPolicyType 23 defaultNSFWPolicy: NSFWPolicyType
25 formValuesWatcher: Subscription 24 formValuesWatcher: Subscription
26 25
26 private allLanguagesGroup: string
27
27 constructor ( 28 constructor (
28 protected formValidatorService: FormValidatorService, 29 protected formValidatorService: FormValidatorService,
29 private authService: AuthService, 30 private authService: AuthService,
@@ -36,6 +37,8 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
36 } 37 }
37 38
38 ngOnInit () { 39 ngOnInit () {
40 this.allLanguagesGroup = this.i18n('All languages')
41
39 let oldForm: any 42 let oldForm: any
40 43
41 this.buildForm({ 44 this.buildForm({
@@ -51,13 +54,15 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
51 this.serverService.getConfig(), 54 this.serverService.getConfig(),
52 this.userInformationLoaded.pipe(first()) 55 this.userInformationLoaded.pipe(first())
53 ]).subscribe(([ languages, config ]) => { 56 ]).subscribe(([ languages, config ]) => {
54 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ] 57 const group = this.allLanguagesGroup
58
59 this.languageItems = [ { label: this.i18n('Unknown language'), id: '_unknown', group } ]
55 this.languageItems = this.languageItems 60 this.languageItems = this.languageItems
56 .concat(languages.map(l => ({ label: l.label, value: l.id }))) 61 .concat(languages.map(l => ({ label: l.label, id: l.id, group })))
57 62
58 const videoLanguages = this.user.videoLanguages 63 const videoLanguages: ItemSelectCheckboxValue[] = this.user.videoLanguages
59 ? this.user.videoLanguages 64 ? this.user.videoLanguages.map(l => ({ id: l }))
60 : this.languageItems.map(l => l.value) 65 : [ { group } ]
61 66
62 this.defaultNSFWPolicy = config.instance.defaultNSFWPolicy 67 this.defaultNSFWPolicy = config.instance.defaultNSFWPolicy
63 68
@@ -71,10 +76,12 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
71 76
72 if (this.reactiveUpdate) { 77 if (this.reactiveUpdate) {
73 oldForm = { ...this.form.value } 78 oldForm = { ...this.form.value }
79
74 this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => { 80 this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => {
75 const updatedKey = Object.keys(formValue).find(k => formValue[k] !== oldForm[k]) 81 const updatedKey = Object.keys(formValue).find(k => formValue[k] !== oldForm[k])
76 oldForm = { ...this.form.value } 82 oldForm = { ...this.form.value }
77 this.updateDetails([updatedKey]) 83
84 this.updateDetails([ updatedKey ])
78 }) 85 })
79 } 86 }
80 }) 87 })
@@ -91,16 +98,24 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
91 const autoPlayNextVideo = this.form.value['autoPlayNextVideo'] 98 const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
92 99
93 let videoLanguages: string[] = this.form.value['videoLanguages'] 100 let videoLanguages: string[] = this.form.value['videoLanguages']
101
94 if (Array.isArray(videoLanguages)) { 102 if (Array.isArray(videoLanguages)) {
95 if (videoLanguages.length === this.languageItems.length) { 103 if (videoLanguages.length > 20) {
96 videoLanguages = null // null means "All" 104 this.notifier.error(this.i18n('Too many languages are enabled. Please enable them all or stay below 20 enabled languages.'))
97 } else if (videoLanguages.length > 20) {
98 this.notifier.error('Too many languages are enabled. Please enable them all or stay below 20 enabled languages.')
99 return 105 return
100 } else if (videoLanguages.length === 0) { 106 }
101 this.notifier.error('You need to enabled at least 1 video language.') 107
108 if (videoLanguages.length === 0) {
109 this.notifier.error(this.i18n('You need to enable at least 1 video language.'))
102 return 110 return
103 } 111 }
112
113 if (
114 videoLanguages.length === this.languageItems.length ||
115 (videoLanguages.length === 1 && videoLanguages[0] === this.allLanguagesGroup)
116 ) {
117 videoLanguages = null // null means "All"
118 }
104 } 119 }
105 120
106 let details: UserUpdateMe = { 121 let details: UserUpdateMe = {
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index ae2b99a5b..e6491b492 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -304,6 +304,17 @@
304 z-index: 100; 304 z-index: 100;
305} 305}
306 306
307
308@mixin ng-select ($width) {
309 ::ng-deep ng-select {
310 width: $width;
311
312 @media screen and (max-width: $width) {
313 width: 100%;
314 }
315 }
316}
317
307@mixin peertube-select-container ($width) { 318@mixin peertube-select-container ($width) {
308 padding: 0; 319 padding: 0;
309 margin: 0; 320 margin: 0;
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index bf49639f5..750c433b5 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -303,67 +303,6 @@ p-table {
303 } 303 }
304} 304}
305 305
306// multiselect customizations
307p-multiselect {
308 .ui-multiselect {
309 border-color: #C6C6C6;
310
311 &:not(.ui-state-disabled) {
312 &:hover {
313 border-color: #C6C6C6;
314 }
315
316 &:focus,
317 &.ui-state-focus {
318 box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
319 }
320 }
321 }
322
323 .ui-multiselect-label {
324 font-size: 15px !important;
325 padding: 4px 30px 4px 12px !important;
326
327 $width: 338px;
328 width: $width !important;
329
330 @media screen and (max-width: $width) {
331 width: 100% !important;
332 }
333 }
334
335 .pi.pi-chevron-down {
336 margin-left: 0 !important;
337
338 &::after {
339 @include select-arrow-down;
340
341 right: 0;
342 margin-top: 6px;
343 }
344 }
345
346 .ui-chkbox-icon {
347 //position: absolute !important;
348 width: 18px;
349 height: 18px;
350 //left: 0;
351
352 //&::after {
353 // left: -2px !important;
354 //}
355 }
356
357 .ui-multiselect-panel .ui-multiselect-items .ui-multiselect-item.ui-state-highlight {
358 background-color: pvar(--mainColorLighter);
359 }
360
361 .ui-inputtext:enabled:focus:not(.ui-state-error) {
362 border-color: pvar(--mainColorLighter) !important;
363 box-shadow: none;
364 }
365}
366
367// PrimeNG calendar tweaks 306// PrimeNG calendar tweaks
368p-calendar .ui-datepicker { 307p-calendar .ui-datepicker {
369 a { 308 a {