diff options
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 | ||
33 | my-select-checkbox { | ||
34 | @include ng-select($form-base-input-width); | ||
35 | } | ||
36 | |||
33 | input[type=submit] { | 37 | input[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 @@ | |||
1 | import { SelectItem } from 'primeng/api' | ||
2 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin } from 'rxjs' |
3 | import { ViewportScroller } from '@angular/common' | 2 | import { ViewportScroller } from '@angular/common' |
4 | import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core' | 3 | import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core' |
5 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
6 | import { Notifier } from '@app/core' | 5 | import { Notifier } from '@app/core' |
7 | import { ServerService } from '@app/core/server/server.service' | 6 | import { ServerService } from '@app/core/server/server.service' |
8 | import { CustomConfigValidatorsService, FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms' | 7 | import { |
8 | CustomConfigValidatorsService, | ||
9 | FormReactive, | ||
10 | FormValidatorService, | ||
11 | SelectOptionsItem, | ||
12 | UserValidatorsService | ||
13 | } from '@app/shared/shared-forms' | ||
9 | import { NgbNav } from '@ng-bootstrap/ng-bootstrap' | 14 | import { NgbNav } from '@ng-bootstrap/ng-bootstrap' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 15 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { CustomConfig, ServerConfig } from '@shared/models' | 16 | import { 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 @@ | |||
1 | import { FormReactive } from '@app/shared/shared-forms' | 1 | import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms' |
2 | import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component' | ||
3 | import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' | 2 | import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models' |
4 | import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model' | 3 | import { 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 | |||
4 | import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' | 4 | import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' |
5 | import { ServerService } from '@app/core' | 5 | import { ServerService } from '@app/core' |
6 | import { removeElementFromArray } from '@app/helpers' | 6 | import { removeElementFromArray } from '@app/helpers' |
7 | import { FormReactiveValidationMessages, FormValidatorService, VideoValidatorsService } from '@app/shared/shared-forms' | 7 | import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem, VideoValidatorsService } from '@app/shared/shared-forms' |
8 | import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component' | ||
9 | import { InstanceService } from '@app/shared/shared-instance' | 8 | import { InstanceService } from '@app/shared/shared-instance' |
10 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' | 9 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { 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' | |||
2 | import { Directive, EventEmitter, OnInit } from '@angular/core' | 2 | import { Directive, EventEmitter, OnInit } from '@angular/core' |
3 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' | 3 | import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core' |
4 | import { populateAsyncUserVideoChannels } from '@app/helpers' | 4 | import { populateAsyncUserVideoChannels } from '@app/helpers' |
5 | import { FormReactive } from '@app/shared/shared-forms' | 5 | import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms' |
6 | import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component' | ||
7 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
9 | import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' | 8 | import { 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' | |||
2 | import { Component, HostListener, OnInit } from '@angular/core' | 2 | import { Component, HostListener, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { Notifier } from '@app/core' | 4 | import { Notifier } from '@app/core' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' |
6 | import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component' | ||
7 | import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 8 | import { 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 @@ | |||
1 | import { DatePipe } from '@angular/common' | 1 | import { DatePipe } from '@angular/common' |
2 | import { SelectChannelItem } from '@app/shared/shared-forms' | ||
2 | import { environment } from '../../environments/environment' | 3 | import { environment } from '../../environments/environment' |
3 | import { AuthService } from '../core/auth' | 4 | import { AuthService } from '../core/auth' |
4 | import { 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 |
7 | function getParameterByName (name: string, url: string) { | 7 | function 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 @@ | |||
1 | export * from './form-validators' | 1 | export * from './form-validators' |
2 | export * from './form-reactive' | 2 | export * from './form-reactive' |
3 | export * from './select' | ||
3 | export * from './input-readonly-copy.component' | 4 | export * from './input-readonly-copy.component' |
4 | export * from './markdown-textarea.component' | 5 | export * from './markdown-textarea.component' |
5 | export * from './peertube-checkbox.component' | 6 | export * 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 | |||
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/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 @@ | |||
1 | export * from './select-channel.component' | ||
2 | export * from './select-options.component' | ||
3 | export * from './select-tags.component' | ||
4 | export * 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 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, forwardRef, Input } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Actor } from '../shared-main' | 3 | import { Actor } from '@app/shared/shared-main/account/actor.model' |
4 | 4 | ||
5 | export type SelectChannelItem = { | 5 | export 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 | |||
4 | ng-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 @@ | |||
1 | import { Component, Input, forwardRef } from '@angular/core' | ||
2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' | ||
3 | import { SelectOptionsItem } from './select-options.component' | ||
4 | |||
5 | export 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 | }) | ||
19 | export 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 @@ | |||
1 | import { Component, Input, forwardRef } from '@angular/core' | 1 | import { Component, Input, forwardRef } from '@angular/core' |
2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' | 2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms' |
3 | 3 | ||
4 | export type SelectOptionsItem = { id: number | string, label: string, description?: string } | 4 | export 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 | |||
6 | ng-select { | ||
7 | width: $form-base-input-width; | ||
8 | |||
9 | @media screen and (max-width: $form-base-input-width) { | ||
10 | width: 100%; | ||
11 | } | ||
12 | } | ||
13 | |||
14 | ng-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' | |||
3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
4 | import { InputMaskModule } from 'primeng/inputmask' | 4 | import { InputMaskModule } from 'primeng/inputmask' |
5 | import { InputSwitchModule } from 'primeng/inputswitch' | 5 | import { InputSwitchModule } from 'primeng/inputswitch' |
6 | import { MultiSelectModule } from 'primeng/multiselect' | ||
7 | import { NgSelectModule } from '@ng-select/ng-select' | 6 | import { NgSelectModule } from '@ng-select/ng-select' |
8 | import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service' | 7 | import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service' |
9 | import { SharedGlobalIconModule } from '../shared-icons' | 8 | import { SharedGlobalIconModule } from '../shared-icons' |
@@ -32,9 +31,7 @@ import { PreviewUploadComponent } from './preview-upload.component' | |||
32 | import { ReactiveFileComponent } from './reactive-file.component' | 31 | import { ReactiveFileComponent } from './reactive-file.component' |
33 | import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' | 32 | import { TextareaAutoResizeDirective } from './textarea-autoresize.directive' |
34 | import { TimestampInputComponent } from './timestamp-input.component' | 33 | import { TimestampInputComponent } from './timestamp-input.component' |
35 | import { SelectChannelComponent } from './select-channel.component' | 34 | import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select' |
36 | import { SelectOptionsComponent } from './select-options.component' | ||
37 | import { 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 | ||
22 | my-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 @@ | |||
1 | import { pick } from 'lodash-es' | 1 | import { pick } from 'lodash-es' |
2 | import { SelectItem } from 'primeng/api' | ||
3 | import { forkJoin, Subject, Subscription } from 'rxjs' | 2 | import { forkJoin, Subject, Subscription } from 'rxjs' |
4 | import { first } from 'rxjs/operators' | 3 | import { first } from 'rxjs/operators' |
5 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' | 4 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' |
6 | import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' |
7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormValidatorService, ItemSelectCheckboxValue, SelectOptionsItem } from '@app/shared/shared-forms' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { UserUpdateMe } from '@shared/models' | 8 | import { UserUpdateMe } from '@shared/models' |
10 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' | 9 | import { 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 | ||
307 | p-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 |
368 | p-calendar .ui-datepicker { | 307 | p-calendar .ui-datepicker { |
369 | a { | 308 | a { |