]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Use ng select for multiselect
authorChocobozzz <me@florianbigard.com>
Tue, 11 Aug 2020 14:07:53 +0000 (16:07 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 11 Aug 2020 14:18:42 +0000 (16:18 +0200)
31 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.scss
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-edit.ts
client/src/app/+videos/+video-edit/shared/video-edit.component.ts
client/src/app/+videos/+video-edit/video-add-components/video-send.ts
client/src/app/+videos/+video-edit/video-update.component.ts
client/src/app/helpers/utils.ts
client/src/app/shared/shared-forms/index.ts
client/src/app/shared/shared-forms/select-shared.component.scss [deleted file]
client/src/app/shared/shared-forms/select/index.ts [new file with mode: 0644]
client/src/app/shared/shared-forms/select/select-channel.component.html [moved from client/src/app/shared/shared-forms/select-channel.component.html with 100% similarity]
client/src/app/shared/shared-forms/select/select-channel.component.ts [moved from client/src/app/shared/shared-forms/select-channel.component.ts with 94% similarity]
client/src/app/shared/shared-forms/select/select-checkbox.component.html [new file with mode: 0644]
client/src/app/shared/shared-forms/select/select-checkbox.component.scss [new file with mode: 0644]
client/src/app/shared/shared-forms/select/select-checkbox.component.ts [new file with mode: 0644]
client/src/app/shared/shared-forms/select/select-options.component.html [moved from client/src/app/shared/shared-forms/select-options.component.html with 89% similarity]
client/src/app/shared/shared-forms/select/select-options.component.ts [moved from client/src/app/shared/shared-forms/select-options.component.ts with 85% similarity]
client/src/app/shared/shared-forms/select/select-shared.component.scss [new file with mode: 0644]
client/src/app/shared/shared-forms/select/select-tags.component.html [moved from client/src/app/shared/shared-forms/select-tags.component.html with 100% similarity]
client/src/app/shared/shared-forms/select/select-tags.component.scss [moved from client/src/app/shared/shared-forms/select-tags.component.scss with 100% similarity]
client/src/app/shared/shared-forms/select/select-tags.component.ts [moved from client/src/app/shared/shared-forms/select-tags.component.ts with 96% similarity]
client/src/app/shared/shared-forms/shared-form.module.ts
client/src/app/shared/shared-user-settings/user-video-settings.component.html
client/src/app/shared/shared-user-settings/user-video-settings.component.scss
client/src/app/shared/shared-user-settings/user-video-settings.component.ts
client/src/sass/include/_mixins.scss
client/src/sass/primeng-custom.scss

index 8fdced1c7c710d46777d29f6394d6ceabfabb4a5..b82281c6cdda5d6733b381e011f6f2750eecc53b 100644 (file)
                 <label i18n for="instanceCategories">Main instance categories</label>
 
                 <div>
-                  <p-multiSelect
-                    inputId="instanceCategories" [options]="categoryItems" formControlName="categories" [showToggleAll]="false"
-                    [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()"
-                    emptyFilterMessage="No results found" i18n-emptyFilterMessage
-                  ></p-multiSelect>
+                  <my-select-checkbox
+                    id="instanceCategories"
+                    formControlName="categories" [availableItems]="categoryItems"
+                    [selectableGroup]="false"
+                  >
+                  </my-select-checkbox>
                 </div>
               </div>
 
                 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
 
                 <div>
-                  <p-multiSelect
-                    inputId="instanceLanguages" [options]="languageItems" formControlName="languages" [showToggleAll]="false"
-                    [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()"
-                    emptyFilterMessage="No results found" i18n-emptyFilterMessage
-                  ></p-multiSelect>
+                  <my-select-checkbox
+                    id="instanceLanguages"
+                    formControlName="languages" [availableItems]="languageItems"
+                    [selectableGroup]="false"
+                  >
+                  </my-select-checkbox>
                 </div>
               </div>
 
index 9618100b51a81466962e3b67f6c07d55199ea186..f8f2d5fdc3ef0b80a89ebfc13b21336d0bef480c 100644 (file)
@@ -30,6 +30,10 @@ input[type=checkbox] {
   @include peertube-select-container($form-base-input-width);
 }
 
+my-select-checkbox {
+  @include ng-select($form-base-input-width);
+}
+
 input[type=submit] {
   @include peertube-button;
   @include orange-button;
index 69629770f518b7f704d1610796ba50b9bfd08eb9..00a0bfad201c47633630a8f31657ae52ab4a9423 100644 (file)
@@ -1,11 +1,16 @@
-import { SelectItem } from 'primeng/api'
 import { forkJoin } from 'rxjs'
 import { ViewportScroller } from '@angular/common'
 import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core'
 import { ConfigService } from '@app/+admin/config/shared/config.service'
 import { Notifier } from '@app/core'
 import { ServerService } from '@app/core/server/server.service'
-import { CustomConfigValidatorsService, FormReactive, FormValidatorService, UserValidatorsService } from '@app/shared/shared-forms'
+import {
+  CustomConfigValidatorsService,
+  FormReactive,
+  FormValidatorService,
+  SelectOptionsItem,
+  UserValidatorsService
+} from '@app/shared/shared-forms'
 import { NgbNav } from '@ng-bootstrap/ng-bootstrap'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { CustomConfig, ServerConfig } from '@shared/models'
@@ -25,8 +30,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
   resolutions: { id: string, label: string, description?: string }[] = []
   transcodingThreadOptions: { label: string, value: number }[] = []
 
-  languageItems: SelectItem[] = []
-  categoryItems: SelectItem[] = []
+  languageItems: SelectOptionsItem[] = []
+  categoryItems: SelectOptionsItem[] = []
 
   private serverConfig: ServerConfig
 
@@ -290,22 +295,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
       )
   }
 
-  getSelectedLanguageLabel () {
-    return this.i18n('{{\'{0} languages selected')
-  }
-
-  getDefaultLanguageLabel () {
-    return this.i18n('No language')
-  }
-
-  getSelectedCategoryLabel () {
-    return this.i18n('{{\'{0} categories selected')
-  }
-
-  getDefaultCategoryLabel () {
-    return this.i18n('No category')
-  }
-
   gotoAnchor () {
     const hashToNav = {
       'customizations': 'advanced-configuration'
@@ -331,8 +320,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
       ([ config, languages, categories ]) => {
         this.customConfig = config
 
-        this.languageItems = languages.map(l => ({ label: l.label, value: l.id }))
-        this.categoryItems = categories.map(l => ({ label: l.label, value: l.id }))
+        this.languageItems = languages.map(l => ({ label: l.label, id: l.id }))
+        this.categoryItems = categories.map(l => ({ label: l.label, id: l.id + '' }))
 
         this.updateForm()
         // Force form validation
index 3090247e582345d4f6a2f53c35de820901259e6b..d02f6526fa07ab59a912c445de6de77d424038b8 100644 (file)
           <div role="menu" class="dropdown-menu" ngbDropdownMenu>
             <div class="dropdown-header" i18n>Table parameters</div>
             <div ngbDropdownItem class="dropdown-item">
-              <p-multiSelect
-                [options]="columns" [showToggleAll]="true" [(ngModel)]="selectedColumns" optionLabel="label"
-                emptyFilterMessage="No matching column found" i18n-emptyFilterMessage [filter]="false"
-                selectedItemsLabel="{0} columns displayed" i18n-emptyFilterMessage [showHeader]="false"
-                [maxSelectedLabels]="4"
-              ></p-multiSelect>
+              <my-select-checkbox
+                name="columns"
+                [availableItems]="columns"
+                [selectableGroup]="false" [(ngModel)]="selectedColumns"
+              >
+              </my-select-checkbox>
             </div>
             <div ngbDropdownItem class="dropdown-item">
               <my-peertube-checkbox inputName="highlightBannedUsers" [(ngModel)]="highlightBannedUsers"
           </div>
         </div>
       </th>
-      <th *ngIf="getColumn('username')" pResizableColumn i18n pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th>
-      <th *ngIf="getColumn('email')" i18n>{{ getColumn('email').label }}</th>
-      <th *ngIf="getColumn('quota')" style="width: 160px;" i18n pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th>
-      <th *ngIf="getColumn('quotaDaily')" style="width: 160px;" i18n>{{ getColumn('quotaDaily').label }}</th>
-      <th *ngIf="getColumn('role')" style="width: 120px;" i18n pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th>
-      <th *ngIf="getColumn('pluginAuth')" style="width: 140px;" pResizableColumn i18n>{{ getColumn('pluginAuth').label }}</th>
-      <th *ngIf="getColumn('createdAt')" style="width: 150px;" i18n pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th *ngIf="getColumn('lastLoginDate')" style="width: 150px;" i18n pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th>
+      <th *ngIf="isSelected('username')" pResizableColumn i18n pSortableColumn="username">{{ getColumn('username').label }} <p-sortIcon field="username"></p-sortIcon></th>
+      <th *ngIf="isSelected('email')" i18n>{{ getColumn('email').label }}</th>
+      <th *ngIf="isSelected('quota')" style="width: 160px;" i18n pSortableColumn="videoQuotaUsed">{{ getColumn('quota').label }} <p-sortIcon field="videoQuotaUsed"></p-sortIcon></th>
+      <th *ngIf="isSelected('quotaDaily')" style="width: 160px;" i18n>{{ getColumn('quotaDaily').label }}</th>
+      <th *ngIf="isSelected('role')" style="width: 120px;" i18n pSortableColumn="role">{{ getColumn('role').label }} <p-sortIcon field="role"></p-sortIcon></th>
+      <th *ngIf="isSelected('pluginAuth')" style="width: 140px;" pResizableColumn i18n>{{ getColumn('pluginAuth').label }}</th>
+      <th *ngIf="isSelected('createdAt')" style="width: 150px;" i18n pSortableColumn="createdAt">{{ getColumn('createdAt').label }} <p-sortIcon field="createdAt"></p-sortIcon></th>
+      <th *ngIf="isSelected('lastLoginDate')" style="width: 150px;" i18n pSortableColumn="lastLoginDate">{{ getColumn('lastLoginDate').label }} <p-sortIcon field="lastLoginDate"></p-sortIcon></th>
     </tr>
   </ng-template>
 
         </my-user-moderation-dropdown>
       </td>
 
-      <td *ngIf="getColumn('username')">
+      <td *ngIf="isSelected('username')">
         <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
           <div class="chip two-lines">
             <img
         </a>
       </td>
 
-      <td *ngIf="getColumn('email')" [title]="user.email">
+      <td *ngIf="isSelected('email')" [title]="user.email">
         <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">
           <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
         </ng-container>
         </ng-template>
       </ng-template>
 
-      <td *ngIf="getColumn('quota')">
+      <td *ngIf="isSelected('quota')">
         <div class="progress" i18n-title title="Total video quota">
           <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
                [attr.aria-valuenow]="user.rawVideoQuotaUsed" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuota">
         </div>
       </td>
 
-      <td *ngIf="getColumn('quotaDaily')">
+      <td *ngIf="isSelected('quotaDaily')">
         <div class="progress" i18n-title title="Total daily video quota">
           <div class="progress-bar secondary" role="progressbar" [style]="{ width: getUserVideoQuotaDailyPercentage(user) + '%' }"
                [attr.aria-valuenow]="user.rawVideoQuotaUsedDaily" aria-valuemin="0" [attr.aria-valuemax]="user.rawVideoQuotaDaily">
         </div>
       </td>
 
-      <td *ngIf="getColumn('role')">
+      <td *ngIf="isSelected('role')">
         <span *ngIf="user.blocked" class="badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span>
         <span *ngIf="!user.blocked" class="badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span>
       </td>
 
-      <td *ngIf="getColumn('pluginAuth')">
+      <td *ngIf="isSelected('pluginAuth')">
         <ng-container *ngIf="user.pluginAuth">{{ user.pluginAuth }}</ng-container>
       </td>
 
-      <td *ngIf="getColumn('createdAt')" [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td>
+      <td *ngIf="isSelected('createdAt')" [title]="user.createdAt">{{ user.createdAt | date: 'short' }}</td>
 
-      <td *ngIf="getColumn('lastLoginDate')" [title]="user.lastLoginDate">{{ user.lastLoginDate | date: 'short' }}</td>
+      <td *ngIf="isSelected('lastLoginDate')" [title]="user.lastLoginDate">{{ user.lastLoginDate | date: 'short' }}</td>
     </tr>
   </ng-template>
 
index 98fc4d0279c1a0bc20e4279060aa3e39c1aa9758..50080bad61ec8d9c04d9e3a930658662e02a6006 100644 (file)
@@ -61,6 +61,7 @@ my-global-icon {
 
 .input-group {
   @include peertube-input-group(300px);
+
   input {
     flex: 1;
   }
index 699b2a6da302c49055ca7e3e2d5e9f64c9c52c36..69d4e917dfbc1e01acf4fb70a8df140efeb88746 100644 (file)
@@ -30,9 +30,9 @@ export class UserListComponent extends RestTable implements OnInit {
 
   selectedUsers: User[] = []
   bulkUserActions: DropdownAction<User[]>[][] = []
-  columns: { key: string, label: string }[]
+  columns: { id: string, label: string }[]
 
-  private _selectedColumns: { key: string, label: string }[]
+  private _selectedColumns: string[]
   private serverConfig: ServerConfig
 
   constructor (
@@ -60,7 +60,7 @@ export class UserListComponent extends RestTable implements OnInit {
     return this._selectedColumns
   }
 
-  set selectedColumns (val) {
+  set selectedColumns (val: string[]) {
     this._selectedColumns = val
   }
 
@@ -112,16 +112,18 @@ export class UserListComponent extends RestTable implements OnInit {
     ]
 
     this.columns = [
-      { key: 'username', label: 'Username' },
-      { key: 'email', label: 'Email' },
-      { key: 'quota', label: 'Video quota' },
-      { key: 'role', label: 'Role' },
-      { key: 'createdAt', label: 'Created' }
+      { id: 'username', label: 'Username' },
+      { id: 'email', label: 'Email' },
+      { id: 'quota', label: 'Video quota' },
+      { id: 'role', label: 'Role' },
+      { id: 'createdAt', label: 'Created' }
     ]
-    this.selectedColumns = [ ...this.columns ] // make a full copy of the array
-    this.columns.push({ key: 'quotaDaily', label: 'Daily quota' })
-    this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' })
-    this.columns.push({ key: 'lastLoginDate', label: 'Last login' })
+
+    this.selectedColumns = this.columns.map(c => c.id)
+
+    this.columns.push({ id: 'quotaDaily', label: 'Daily quota' })
+    this.columns.push({ id: 'pluginAuth', label: 'Auth plugin' })
+    this.columns.push({ id: 'lastLoginDate', label: 'Last login' })
   }
 
   getIdentifier () {
@@ -139,8 +141,12 @@ export class UserListComponent extends RestTable implements OnInit {
     }
   }
 
-  getColumn (key: string) {
-    return this.selectedColumns.find((col: { key: string }) => col.key === key)
+  isSelected (id: string) {
+    return this.selectedColumns.find(c => c === id)
+  }
+
+  getColumn (id: string) {
+    return this.columns.find(c => c.id === id)
   }
 
   getUserVideoQuotaPercentage (user: UserForList) {
index 94a0f616817d3b5215e2f08cf9c3db84a5ef7273..774d58c90b91334d3fcb6e377577f326bed5cf99 100644 (file)
@@ -1,5 +1,4 @@
-import { FormReactive } from '@app/shared/shared-forms'
-import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
+import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
 import { VideoConstant, VideoPlaylistPrivacy } from '@shared/models'
 import { VideoPlaylist } from '@shared/models/videos/playlist/video-playlist.model'
 
index 48c5508f4616bcd040de506a26c0160652935ec0..ba3b7c96a1cd0700fa4b2c559dd1e194dbf97d41 100644 (file)
@@ -4,8 +4,7 @@ import { Component, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular
 import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'
 import { ServerService } from '@app/core'
 import { removeElementFromArray } from '@app/helpers'
-import { FormReactiveValidationMessages, FormValidatorService, VideoValidatorsService } from '@app/shared/shared-forms'
-import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
+import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem, VideoValidatorsService } from '@app/shared/shared-forms'
 import { InstanceService } from '@app/shared/shared-instance'
 import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { I18n } from '@ngx-translate/i18n-polyfill'
index c55e5f9233ae557834b11ff0dd641fc7c717a550..812936d7a4d2549654c81c1e09325b54234c5690 100644 (file)
@@ -2,8 +2,7 @@ import { catchError, switchMap, tap } from 'rxjs/operators'
 import { Directive, EventEmitter, OnInit } from '@angular/core'
 import { AuthService, CanComponentDeactivateResult, Notifier, ServerService } from '@app/core'
 import { populateAsyncUserVideoChannels } from '@app/helpers'
-import { FormReactive } from '@app/shared/shared-forms'
-import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
+import { FormReactive, SelectChannelItem } from '@app/shared/shared-forms'
 import { VideoCaptionEdit, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
 import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
index 263c71f3b813ec1eda420180eb55510b95c76535..abd08f05c17c8819bc152397d7dd98e02f824dbc 100644 (file)
@@ -2,8 +2,7 @@ import { map, switchMap } from 'rxjs/operators'
 import { Component, HostListener, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { Notifier } from '@app/core'
-import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
-import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
+import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms'
 import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
 import { LoadingBarService } from '@ngx-loading-bar/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
index b925e6d987350580acfdd0321157c9578ee7f5b9..d05541ca928e1d4d0cd4567e2a583d9717166310 100644 (file)
@@ -1,7 +1,7 @@
 import { DatePipe } from '@angular/common'
+import { SelectChannelItem } from '@app/shared/shared-forms'
 import { environment } from '../../environments/environment'
 import { AuthService } from '../core/auth'
-import { SelectChannelItem } from '@app/shared/shared-forms/select-channel.component'
 
 // Thanks: https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
 function getParameterByName (name: string, url: string) {
index aa0ee015a594b69bcf89a785590424ce84de6737..747df65cf7399bdd471445ba82c243e3e38d925b 100644 (file)
@@ -1,5 +1,6 @@
 export * from './form-validators'
 export * from './form-reactive'
+export * from './select'
 export * from './input-readonly-copy.component'
 export * from './markdown-textarea.component'
 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 (file)
index 4f231d2..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-$width-size: auto;
-
-ng-select {
-  width: $width-size;
-  @media screen and (max-width: $width-size) {
-    width: 100%;
-  }
-}
-
-// make sure the image is vertically adjusted
-ng-select ::ng-deep .ng-value-label img {
-  position: relative;
-  top: -1px;
-}
-
-ng-select ::ng-deep img {
-  border-radius: 50%;
-  height: 20px;
-  width: 20px;
-}
diff --git a/client/src/app/shared/shared-forms/select/index.ts b/client/src/app/shared/shared-forms/select/index.ts
new file mode 100644 (file)
index 0000000..33459b2
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './select-channel.component'
+export * from './select-options.component'
+export * from './select-tags.component'
+export * from './select-checkbox.component'
similarity index 94%
rename from client/src/app/shared/shared-forms/select-channel.component.ts
rename to client/src/app/shared/shared-forms/select/select-channel.component.ts
index ef4192095dbeff601c86352d25b57257bac103f7..1b0db9b6fc18785ca2bcf3bc46169c84f777791f 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, forwardRef, Input } from '@angular/core'
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
-import { Actor } from '../shared-main'
+import { Actor } from '@app/shared/shared-main/account/actor.model'
 
 export type SelectChannelItem = {
   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 (file)
index 0000000..3f81dd1
--- /dev/null
@@ -0,0 +1,41 @@
+<ng-select
+  [items]="availableItems"
+  [(ngModel)]="selectedItems"
+  (ngModelChange)="onModelChange()"
+  i18n-placeholder placeholder="Add a new language"
+  [clearable]="true"
+  [multiple]="true"
+  [searchable]="true"
+  [closeOnSelect]="false"
+
+  bindValue="id"
+  bindLabel="label"
+
+  notFoundText="No items found" i18n-notFoundText
+
+  [selectableGroup]="selectableGroup"
+  [selectableGroupAsModel]="selectableGroupAsModel"
+
+  groupBy="group"
+  [compareWith]="compareFn"
+
+  [maxSelectedItems]="maxSelectedItems"
+>
+
+  <ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index">
+    <div class="form-group-checkbox">
+      <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
+      <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
+      <span>{{ item.group }}</span>
+    </div>
+  </ng-template>
+
+  <ng-template ng-option-tmp let-item="item" let-item$="item$" let-index="index">
+    <div class="form-group-checkbox">
+      <input id="item-{{index}}" type="checkbox" [ngModel]="item$.selected"/>
+      <span role="checkbox" [attr.aria-checked]="item$.selected"></span>
+      <span>{{ item.label }}</span>
+    </div>
+  </ng-template>
+
+</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 (file)
index 0000000..145f6b2
--- /dev/null
@@ -0,0 +1,18 @@
+@import '_variables';
+@import '_mixins';
+
+ng-select ::ng-deep {
+  .ng-option {
+    display: flex;
+    align-items: center;
+  }
+
+  .form-group-checkbox {
+    display: flex;
+    align-items: center;
+
+    input {
+      @include peertube-checkbox(1px);
+    }
+  }
+}
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 (file)
index 0000000..93653fe
--- /dev/null
@@ -0,0 +1,75 @@
+import { Component, Input, forwardRef } from '@angular/core'
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
+import { SelectOptionsItem } from './select-options.component'
+
+export type ItemSelectCheckboxValue = { id?: string | number, group?: string } | string
+
+@Component({
+  selector: 'my-select-checkbox',
+  styleUrls: [ './select-shared.component.scss', 'select-checkbox.component.scss' ],
+  templateUrl: './select-checkbox.component.html',
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => SelectCheckboxComponent),
+      multi: true
+    }
+  ]
+})
+export class SelectCheckboxComponent implements ControlValueAccessor {
+  @Input() availableItems: SelectOptionsItem[] = []
+  @Input() selectedItems: ItemSelectCheckboxValue[] = []
+  @Input() selectableGroup: boolean
+  @Input() selectableGroupAsModel: boolean
+  @Input() maxSelectedItems: number
+
+  propagateChange = (_: any) => { /* empty */ }
+
+  writeValue (items: ItemSelectCheckboxValue[]) {
+    if (Array.isArray(items)) {
+      this.selectedItems = items.map(i => {
+        if (typeof i === 'string' || typeof i === 'number') {
+          return i + ''
+        }
+
+        if (i.group) {
+          return { group: i.group }
+        }
+
+        return { id: i.id + '' }
+      })
+    } else {
+      this.selectedItems = items
+    }
+
+    this.propagateChange(this.selectedItems)
+  }
+
+  registerOnChange (fn: (_: any) => void) {
+    this.propagateChange = fn
+  }
+
+  registerOnTouched () {
+    // Unused
+  }
+
+  onModelChange () {
+    this.propagateChange(this.selectedItems)
+  }
+
+  compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
+    if (typeof selected === 'string') {
+      return item.id === selected
+    }
+
+    if (this.selectableGroup && item.group && selected.group) {
+      return item.group === selected.group
+    }
+
+    if (selected.id && item.id) {
+      return item.id === selected.id
+    }
+
+    return false
+  }
+}
similarity index 89%
rename from client/src/app/shared/shared-forms/select-options.component.html
rename to client/src/app/shared/shared-forms/select/select-options.component.html
index fda0c2c56eb7e4990c940ed6582f9bc57e37a11b..48eca1cf542c427627c17b2f2dd6df9a5ced93d1 100644 (file)
@@ -3,10 +3,11 @@
   [groupBy]="groupBy"
   [(ngModel)]="selectedId"
   (ngModelChange)="onModelChange()"
-  [bindLabel]="bindLabel"
-  [bindValue]="bindValue"
   [clearable]="clearable"
   [searchable]="searchable"
+
+  bindLabel="label"
+  bindValue="id"
 >
   <ng-template ng-option-tmp let-item="item" let-index="index">
     {{ item.label }}
similarity index 85%
rename from client/src/app/shared/shared-forms/select-options.component.ts
rename to client/src/app/shared/shared-forms/select/select-options.component.ts
index 09f7df53b49ba4745b7b08d9ae441c55805cd345..3ba24c732965846813e450398211e9e08bfccf33 100644 (file)
@@ -1,7 +1,13 @@
 import { Component, Input, forwardRef } from '@angular/core'
 import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
 
-export type SelectOptionsItem = { id: number | string, label: string, description?: string }
+export type SelectOptionsItem = {
+  id: string | number
+  label: string
+  description?: string
+  group?: string
+  groupLabel?: string
+}
 
 @Component({
   selector: 'my-select-options',
@@ -19,14 +25,10 @@ export class SelectOptionsComponent implements ControlValueAccessor {
   @Input() items: SelectOptionsItem[] = []
   @Input() clearable = false
   @Input() searchable = false
-  @Input() bindValue = 'id'
   @Input() groupBy: string
 
   selectedId: number | string
 
-  // ng-select options
-  bindLabel = 'label'
-
   propagateChange = (_: any) => { /* empty */ }
 
   writeValue (id: number | string) {
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 (file)
index 0000000..0b4c6b7
--- /dev/null
@@ -0,0 +1,32 @@
+@import '_variables';
+@import '_mixins';
+
+$form-base-input-width: auto;
+
+ng-select {
+  width: $form-base-input-width;
+
+  @media screen and (max-width: $form-base-input-width) {
+    width: 100%;
+  }
+}
+
+ng-select ::ng-deep {
+  .ng-value-container {
+    max-height: 100px;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
+  // make sure the image is vertically adjusted
+  .ng-value-label img {
+    position: relative;
+    top: -1px;
+  }
+
+  img {
+    border-radius: 50%;
+    height: 20px;
+    width: 20px;
+  }
+}
similarity index 96%
rename from client/src/app/shared/shared-forms/select-tags.component.ts
rename to client/src/app/shared/shared-forms/select/select-tags.component.ts
index a8a19d7887fece59054676883003002726aecc28..93d199037856bc1ffd04248ee6416c43c9fc4b78 100644 (file)
@@ -33,8 +33,6 @@ export class SelectTagsComponent implements ControlValueAccessor {
   }
 
   onModelChange () {
-    console.log(this.selectedItems)
-
     this.propagateChange(this.selectedItems)
   }
 }
index ea62700830c6a4e42d9631fadeea2cb9706d879f..0e0ed5babeebaad2bb85da6c463d3ae9143e7d7c 100644 (file)
@@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'
 import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { InputMaskModule } from 'primeng/inputmask'
 import { InputSwitchModule } from 'primeng/inputswitch'
-import { MultiSelectModule } from 'primeng/multiselect'
 import { NgSelectModule } from '@ng-select/ng-select'
 import { BatchDomainsValidatorsService } from '@app/shared/shared-forms/form-validators/batch-domains-validators.service'
 import { SharedGlobalIconModule } from '../shared-icons'
@@ -32,9 +31,7 @@ import { PreviewUploadComponent } from './preview-upload.component'
 import { ReactiveFileComponent } from './reactive-file.component'
 import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
 import { TimestampInputComponent } from './timestamp-input.component'
-import { SelectChannelComponent } from './select-channel.component'
-import { SelectOptionsComponent } from './select-options.component'
-import { SelectTagsComponent } from './select-tags.component'
+import { SelectChannelComponent, SelectCheckboxComponent, SelectOptionsComponent, SelectTagsComponent } from './select'
 
 @NgModule({
   imports: [
@@ -43,7 +40,6 @@ import { SelectTagsComponent } from './select-tags.component'
 
     InputMaskModule,
     InputSwitchModule,
-    MultiSelectModule,
     NgSelectModule,
 
     SharedMainModule,
@@ -58,9 +54,11 @@ import { SelectTagsComponent } from './select-tags.component'
     ReactiveFileComponent,
     TextareaAutoResizeDirective,
     TimestampInputComponent,
+
     SelectChannelComponent,
     SelectOptionsComponent,
-    SelectTagsComponent
+    SelectTagsComponent,
+    SelectCheckboxComponent
   ],
 
   exports: [
@@ -69,7 +67,6 @@ import { SelectTagsComponent } from './select-tags.component'
 
     InputMaskModule,
     InputSwitchModule,
-    MultiSelectModule,
     NgSelectModule,
 
     InputReadonlyCopyComponent,
@@ -79,9 +76,11 @@ import { SelectTagsComponent } from './select-tags.component'
     ReactiveFileComponent,
     TextareaAutoResizeDirective,
     TimestampInputComponent,
+
     SelectChannelComponent,
     SelectOptionsComponent,
-    SelectTagsComponent
+    SelectTagsComponent,
+    SelectCheckboxComponent
   ],
 
   providers: [
index bb9f5907055a07a75af5253f9ea4b4fef34b4a0c..655b49e18c7296574b8be5c047b203775ec78c22 100644 (file)
     </my-help>
 
     <div>
-      <p-multiSelect
-        inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" [showToggleAll]="true"
-        [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
-        emptyFilterMessage="No results found" i18n-emptyFilterMessage
-      ></p-multiSelect>
+      <my-select-checkbox
+        formControlName="videoLanguages" [availableItems]="languageItems"
+        [selectableGroup]="true" [selectableGroupAsModel]="true"
+      >
+      </my-select-checkbox >
     </div>
   </div>
 
index 430250b87590ec10a5383c6685647fecb782871d..d6a17703a8a8fe753f856d065ec68471bd5c9765 100644 (file)
@@ -19,6 +19,10 @@ input[type=submit] {
   margin-bottom: 30px;
 }
 
+my-select-checkbox {
+  @include ng-select(340px);
+}
+
 .form-group-select {
   margin-bottom: 30px;
 }
index 4e4539936f93d17d07014bec2eefb8738a4a1df6..eb340e24dfcb904638723f12079a9ccaf96b4631 100644 (file)
@@ -1,10 +1,9 @@
 import { pick } from 'lodash-es'
-import { SelectItem } from 'primeng/api'
 import { forkJoin, Subject, Subscription } from 'rxjs'
 import { first } from 'rxjs/operators'
 import { Component, Input, OnDestroy, OnInit } from '@angular/core'
 import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
-import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
+import { FormReactive, FormValidatorService, ItemSelectCheckboxValue, SelectOptionsItem } from '@app/shared/shared-forms'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { UserUpdateMe } from '@shared/models'
 import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
@@ -20,10 +19,12 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
   @Input() notifyOnUpdate = true
   @Input() userInformationLoaded: Subject<any>
 
-  languageItems: SelectItem[] = []
+  languageItems: SelectOptionsItem[] = []
   defaultNSFWPolicy: NSFWPolicyType
   formValuesWatcher: Subscription
 
+  private allLanguagesGroup: string
+
   constructor (
     protected formValidatorService: FormValidatorService,
     private authService: AuthService,
@@ -36,6 +37,8 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
   }
 
   ngOnInit () {
+    this.allLanguagesGroup = this.i18n('All languages')
+
     let oldForm: any
 
     this.buildForm({
@@ -51,13 +54,15 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
       this.serverService.getConfig(),
       this.userInformationLoaded.pipe(first())
     ]).subscribe(([ languages, config ]) => {
-      this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
+      const group = this.allLanguagesGroup
+
+      this.languageItems = [ { label: this.i18n('Unknown language'), id: '_unknown', group } ]
       this.languageItems = this.languageItems
-                               .concat(languages.map(l => ({ label: l.label, value: l.id })))
+                               .concat(languages.map(l => ({ label: l.label, id: l.id, group })))
 
-      const videoLanguages = this.user.videoLanguages
-        ? this.user.videoLanguages
-        : this.languageItems.map(l => l.value)
+      const videoLanguages: ItemSelectCheckboxValue[] = this.user.videoLanguages
+        ? this.user.videoLanguages.map(l => ({ id: l }))
+        : [ { group } ]
 
       this.defaultNSFWPolicy = config.instance.defaultNSFWPolicy
 
@@ -71,10 +76,12 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
 
       if (this.reactiveUpdate) {
         oldForm = { ...this.form.value }
+
         this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => {
           const updatedKey = Object.keys(formValue).find(k => formValue[k] !== oldForm[k])
           oldForm = { ...this.form.value }
-          this.updateDetails([updatedKey])
+
+          this.updateDetails([ updatedKey ])
         })
       }
     })
@@ -91,16 +98,24 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
     const autoPlayNextVideo = this.form.value['autoPlayNextVideo']
 
     let videoLanguages: string[] = this.form.value['videoLanguages']
+
     if (Array.isArray(videoLanguages)) {
-      if (videoLanguages.length === this.languageItems.length) {
-        videoLanguages = null // null means "All"
-      } else if (videoLanguages.length > 20) {
-        this.notifier.error('Too many languages are enabled. Please enable them all or stay below 20 enabled languages.')
+      if (videoLanguages.length > 20) {
+        this.notifier.error(this.i18n('Too many languages are enabled. Please enable them all or stay below 20 enabled languages.'))
         return
-      } else if (videoLanguages.length === 0) {
-        this.notifier.error('You need to enabled at least 1 video language.')
+      }
+
+      if (videoLanguages.length === 0) {
+        this.notifier.error(this.i18n('You need to enable at least 1 video language.'))
         return
       }
+
+      if (
+        videoLanguages.length === this.languageItems.length ||
+        (videoLanguages.length === 1 && videoLanguages[0] === this.allLanguagesGroup)
+      ) {
+        videoLanguages = null // null means "All"
+      }
     }
 
     let details: UserUpdateMe = {
index ae2b99a5b5f21bc98e6141004843e8db99ad8468..e6491b4929460456496c89242375f7ef91bd25e7 100644 (file)
   z-index: 100;
 }
 
+
+@mixin ng-select ($width) {
+  ::ng-deep ng-select {
+    width: $width;
+
+    @media screen and (max-width: $width) {
+      width: 100%;
+    }
+  }
+}
+
 @mixin peertube-select-container ($width) {
   padding: 0;
   margin: 0;
index bf49639f56dc9ff98064acbc393ac9b808e680fa..750c433b5cdcffeded3f44b856107369b3030eb8 100644 (file)
@@ -303,67 +303,6 @@ p-table {
   }
 }
 
-// multiselect customizations
-p-multiselect {
-  .ui-multiselect {
-    border-color: #C6C6C6;
-
-    &:not(.ui-state-disabled) {
-      &:hover {
-        border-color: #C6C6C6;
-      }
-
-      &:focus,
-      &.ui-state-focus {
-        box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
-      }
-    }
-  }
-
-  .ui-multiselect-label {
-    font-size: 15px !important;
-    padding: 4px 30px 4px 12px !important;
-
-    $width: 338px;
-    width: $width !important;
-
-    @media screen and (max-width: $width) {
-      width: 100% !important;
-    }
-  }
-
-  .pi.pi-chevron-down {
-    margin-left: 0 !important;
-
-    &::after {
-      @include select-arrow-down;
-
-      right: 0;
-      margin-top: 6px;
-    }
-  }
-
-  .ui-chkbox-icon {
-    //position: absolute !important;
-    width: 18px;
-    height: 18px;
-    //left: 0;
-
-    //&::after {
-    //  left: -2px !important;
-    //}
-  }
-
-  .ui-multiselect-panel .ui-multiselect-items .ui-multiselect-item.ui-state-highlight {
-    background-color: pvar(--mainColorLighter);
-  }
-
-  .ui-inputtext:enabled:focus:not(.ui-state-error) {
-    border-color: pvar(--mainColorLighter) !important;
-    box-shadow: none;
-  }
-}
-
 // PrimeNG calendar tweaks
 p-calendar .ui-datepicker {
   a {