Remove ng-select

Component is too complex and causes accessibility issues
This commit is contained in:
Chocobozzz 2024-10-01 15:19:04 +02:00
parent 9fbad291af
commit d54e5178bc
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
60 changed files with 850 additions and 852 deletions

View File

@ -56,7 +56,6 @@
"@formatjs/intl-locale": "^4.0.0",
"@formatjs/intl-pluralrules": "^5.2.2",
"@ng-bootstrap/ng-bootstrap": "^17.0.0",
"@ng-select/ng-select": "^13.8.1",
"@ngx-loading-bar/core": "^6.0.0",
"@ngx-loading-bar/http-client": "^6.0.0",
"@ngx-loading-bar/router": "^6.0.0",

View File

@ -29,7 +29,7 @@
<my-select-custom-value
labelId="instanceDefaultClientRouteLabel"
labelForId="instanceDefaultClientRoute"
inputId="instanceDefaultClientRoute"
[items]="defaultLandingPageOptions"
formControlName="defaultClientRoute"
inputType="text"
@ -224,7 +224,7 @@
<my-select-custom-value
labelId="userVideoQuotaLabel"
labelForId="userVideoQuota"
inputId="userVideoQuota"
[items]="getVideoQuotaOptions()"
formControlName="videoQuota"
i18n-inputSuffix inputSuffix="bytes" inputType="number"
@ -241,7 +241,7 @@
<my-select-custom-value
labelId="userVideoQuotaDailyLabel"
labelForId="userVideoQuotaDaily"
inputId="userVideoQuotaDaily"
[items]="getVideoQuotaDailyOptions()"
formControlName="videoQuotaDaily"
i18n-inputSuffix inputSuffix="bytes" inputType="number"
@ -561,7 +561,7 @@
<my-select-custom-value
labelId="exportUsersMaxUserVideoQuota"
labelForId="exportUsersMaxUserVideoQuota"
inputId="exportUsersMaxUserVideoQuota"
[items]="exportMaxUserVideoQuotaOptions"
formControlName="maxUserVideoQuota"
i18n-inputSuffix inputSuffix="bytes" inputType="number"
@ -574,10 +574,7 @@
<div class="form-group" [ngClass]="getDisabledExportUsersClass()">
<label i18n for="exportUsersExportExpiration">User export expiration</label>
<my-select-options
labelForId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="false"
></my-select-options>
<my-select-options inputId="exportUsersExportExpiration" [items]="exportExpirationOptions" formControlName="exportExpiration"></my-select-options>
<div i18n class="mt-1 small muted">The archive file is deleted after this period.</div>

View File

@ -56,8 +56,7 @@ input[type=checkbox] {
}
my-select-options,
my-select-custom-value,
my-select-checkbox {
my-select-custom-value {
display: block;
@include responsive-width($form-base-input-width);

View File

@ -76,7 +76,7 @@
<div>
<my-select-checkbox
labelForId="instanceCategories"
inputId="instanceCategories"
formControlName="categories" [availableItems]="categoryItems"
[selectableGroup]="false"
i18n-placeholder placeholder="Add a new category"
@ -90,7 +90,7 @@
<div>
<my-select-checkbox
labelForId="instanceLanguages"
inputId="instanceLanguages"
formControlName="languages" [availableItems]="languageItems"
[selectableGroup]="false"
i18n-placeholder placeholder="Add a new language"

View File

@ -73,10 +73,7 @@
<div class="form-group" [ngClass]="getDisabledLiveClass()">
<label i18n for="liveMaxDuration">Max live duration</label>
<my-select-options
labelForId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"
bindLabel="label" bindValue="value" [clearable]="false" [searchable]="true"
></my-select-options>
<my-select-options inputId="liveMaxDuration" [items]="liveMaxDurationOptions" formControlName="maxDuration"></my-select-options>
<div *ngIf="formErrors.live.maxDuration" class="form-error" role="alert">{{ formErrors.live.maxDuration }}</div>
</div>
@ -178,7 +175,7 @@
<my-select-custom-value
labelId="liveTranscodingThreadsLabel"
labelForId="liveTranscodingThreads"
inputId="liveTranscodingThreads"
[items]="transcodingThreadOptions"
formControlName="threads"
[clearable]="false"
@ -190,13 +187,7 @@
<label i18n for="liveTranscodingProfile">Live transcoding profile</label>
<span class="small muted ms-1" i18n>new live transcoding profiles can be added by PeerTube plugins</span>
<my-select-options
id="liveTranscodingProfile"
formControlName="profile"
[items]="transcodingProfiles"
[clearable]="false"
>
</my-select-options>
<my-select-options inputId="liveTranscodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
<div *ngIf="formErrors.live.transcoding.profile" class="form-error" role="alert">{{ formErrors.live.transcoding.profile }}</div>
</div>

View File

@ -201,7 +201,7 @@
<my-select-custom-value
labelId="transcodingThreadsLabel"
labelForId="transcodingThreads"
inputId="transcodingThreads"
[items]="transcodingThreadOptions"
formControlName="threads"
[clearable]="false"
@ -226,12 +226,7 @@
<label i18n for="transcodingProfile">Transcoding profile</label>
<span class="small muted ms-1" i18n>new transcoding profiles can be added by PeerTube plugins</span>
<my-select-options
id="transcodingProfile"
formControlName="profile"
[items]="transcodingProfiles"
[clearable]="false"
></my-select-options>
<my-select-options inputId="transcodingProfile" formControlName="profile" [items]="transcodingProfiles"></my-select-options>
<div *ngIf="formErrors.transcoding.profile" class="form-error" role="alert">{{ formErrors.transcoding.profile }}</div>
</div>

View File

@ -154,7 +154,7 @@
<my-select-custom-value
labelId="videoQuotaLabel"
labelForId="videoQuota"
inputId="videoQuota"
[items]="videoQuotaOptions"
formControlName="videoQuota"
i18n-inputSuffix inputSuffix="bytes" inputType="number"
@ -173,7 +173,7 @@
<my-select-custom-value
labelId="videoQuotaDailyLabel"
labelForId="videoQuotaDaily"
inputId="videoQuotaDaily"
[items]="videoQuotaDailyOptions"
formControlName="videoQuotaDaily"
i18n-inputSuffix inputSuffix="bytes" inputType="number"

View File

@ -52,7 +52,7 @@
<label i18n for="select-columns">Select the columns to display</label>
<my-select-checkbox
labelForId="select-columns"
inputId="select-columns"
[availableItems]="columns"
[selectableGroup]="false" [(ngModel)]="selectedColumns"
i18n-placeholder placeholder="Select the columns to display"

View File

@ -7,34 +7,19 @@
<div class="select-filter-block">
<label for="jobType" i18n>Job type</label>
<ng-select
class="select-job-type"
labelForId="jobType"
[(ngModel)]="jobType" (ngModelChange)="onJobStateOrTypeChanged()"
[searchable]="true"
[clearable]="false"
>
<ng-option *ngFor="let jobType of jobTypes" [value]="jobType">{{ jobType }}</ng-option>
</ng-select>
<my-select-options
class="select-job-type" inputId="jobType" filter="true"
[items]="jobTypeItems" [(ngModel)]="jobType" (ngModelChange)="onJobStateOrTypeChanged()"
></my-select-options>
</div>
<div class="select-filter-block">
<label for="jobState" i18n>Job state</label>
<ng-select
class="select-job-state"
labelForId="jobState"
[(ngModel)]="jobState"
(ngModelChange)="onJobStateOrTypeChanged()"
[clearable]="false"
[searchable]="false"
>
<ng-option value="all">
<span i18n="Selector for the list displaying jobs, filtering by their state">any</span>
</ng-option>
<ng-option *ngFor="let state of jobStates" [value]="state">
<span class="pt-badge" [ngClass]="getJobStateClass(state)">{{ state }}</span>
</ng-option>
</ng-select>
<my-select-options
class="select-job-state" inputId="jobState"
[items]="jobStateItems" [(ngModel)]="jobState" (ngModelChange)="onJobStateOrTypeChanged()"
></my-select-options>
</div>
<div class="button-filter-block">
@ -78,7 +63,7 @@
<td class="job-priority c-hand" [pRowToggler]="job">{{ job.priority }}</td>
<td class="job-state c-hand" [pRowToggler]="job" *ngIf="jobState === 'all'">
<span class="pt-badge ellipsis" [ngClass]="getJobStateClass(job.state)">{{ job.state }}</span>
<span class="ellipsis" [ngClass]="getJobStateClasses(job.state)">{{ job.state }}</span>
</td>
<td *ngIf="hasGlobalProgress()" class="job-progress c-hand" [pRowToggler]="job">
@ -96,7 +81,7 @@
<ng-template pTemplate="rowexpansion" let-job>
<tr>
<td [attr.colspan]="getColspan()">
<td myAutoColspan>
<pre>{{ [
'Job: ' + job.id,
'Type: ' + job.type,
@ -105,13 +90,15 @@
].join('\n') }}</pre>
</td>
</tr>
<tr>
<td [attr.colspan]="getColspan()">
<td myAutoColspan>
<pre>{{ job.data }}</pre>
</td>
</tr>
<tr class="job-error" *ngIf="job.error">
<td [attr.colspan]="getColspan()">
<td myAutoColspan>
<pre>{{ job.error }}</pre>
</td>
</tr>
@ -119,18 +106,22 @@
<ng-template pTemplate="emptymessage">
<tr>
<td [attr.colspan]="getColspan()">
<td myAutoColspan>
<div class="no-results">
<div class="d-block">
<ng-container *ngIf="jobState === 'all'">
<ng-container *ngIf="jobType === 'all'" i18n>No jobs found.</ng-container>
<ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found.</ng-container>
</ng-container>
<ng-container *ngIf="jobState !== 'all'">
<ng-container *ngIf="jobType === 'all'" i18n>No <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span> jobs found.</ng-container>
<ng-container *ngIf="jobType !== 'all'" i18n>No <code>{{ jobType }}</code> jobs found that are <span class="pt-badge" [ngClass]="getJobStateClass(jobState)">{{ jobState }}</span>.</ng-container>
</ng-container>
@if (jobState === 'all') {
@if (jobType === 'all') {
<ng-container i18n>No jobs found.</ng-container>
} @else {
<ng-container i18n>No <code>{{ jobType }}</code> jobs found.</ng-container>
}
} @else {
@if (jobType === 'all') {
<ng-container i18n>No <span [ngClass]="getJobStateClasses(jobState)">{{ jobState }}</span> jobs found.</ng-container>
} @else {
<ng-container i18n>No <code>{{ jobType }}</code> jobs found that are <span [ngClass]="getJobStateClasses(jobState)">{{ jobState }}</span>.</ng-container>
}
}
</div>
</div>
</td>

View File

@ -2,17 +2,17 @@
@use '_mixins' as *;
.select-job-state {
display: block;
min-width: 120px;
::ng-deep .pt-badge {
font-size: 13px;
}
}
.select-job-type {
display: block;
min-width: 240px;
::ng-deep .ng-dropdown-panel .ng-dropdown-panel-items {
@media screen and (min-height: 500px) {
max-height: calc(90vh - 250px);
}
}
}
@media screen and (min-width: $primeng-breakpoint) {

View File

@ -1,20 +1,21 @@
import { SortMeta, SharedModule } from 'primeng/api'
import { NgClass, NgFor, NgIf } from '@angular/common'
import { Component, OnInit } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { Notifier, RestPagination, RestTable } from '@app/core'
import { escapeHTML } from '@peertube/peertube-core-utils'
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { AutoColspanDirective } from '@app/shared/shared-main/common/auto-colspan.directive'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { Job, JobState, JobType } from '@peertube/peertube-models'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { SharedModule, SortMeta } from 'primeng/api'
import { TableModule } from 'primeng/table'
import { SelectOptionsItem } from 'src/types'
import { JobStateClient } from '../../../../types/job-state-client.type'
import { JobTypeClient } from '../../../../types/job-type-client.type'
import { JobService } from './job.service'
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { TableModule } from 'primeng/table'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgFor, NgClass, NgIf } from '@angular/common'
import { FormsModule } from '@angular/forms'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { TableExpanderIconComponent } from '../../../shared/shared-tables/table-expander-icon.component'
import { JobService } from './job.service'
@Component({
selector: 'my-jobs',
@ -24,7 +25,6 @@ import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.compon
imports: [
FormsModule,
NgFor,
NgSelectModule,
NgClass,
ButtonComponent,
TableModule,
@ -32,15 +32,22 @@ import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.compon
NgIf,
NgbTooltip,
TableExpanderIconComponent,
GlobalIconComponent
GlobalIconComponent,
SelectOptionsComponent,
AutoColspanDirective
]
})
export class JobsComponent extends RestTable implements OnInit {
private static LOCAL_STORAGE_STATE = 'jobs-list-state'
private static LOCAL_STORAGE_TYPE = 'jobs-list-type'
jobState?: JobStateClient | 'all'
jobStates: JobStateClient[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed' ]
jobState?: JobStateClient
jobStates: JobStateClient[] = [ 'all', 'active', 'completed', 'failed', 'waiting', 'delayed' ]
jobStateItems: SelectOptionsItem[] = this.jobStates.map(s => ({
id: s,
label: s,
classes: this.getJobStateClasses(s)
}))
jobType: JobTypeClient = 'all'
jobTypes: JobTypeClient[] = [
@ -74,6 +81,7 @@ export class JobsComponent extends RestTable implements OnInit {
'video-transcription',
'videos-views-stats'
]
jobTypeItems: SelectOptionsItem[] = this.jobTypes.map(i => ({ id: i, label: i }))
jobs: Job[] = []
totalRecords: number
@ -96,27 +104,21 @@ export class JobsComponent extends RestTable implements OnInit {
return 'JobsComponent'
}
getJobStateClass (state: JobStateClient) {
getJobStateClasses (state: JobStateClient) {
switch (state) {
case 'active':
return 'badge-blue'
return [ 'pt-badge', 'badge-blue' ]
case 'completed':
return 'badge-green'
return [ 'pt-badge', 'badge-green' ]
case 'delayed':
return 'badge-brown'
return [ 'pt-badge', 'badge-brown' ]
case 'failed':
return 'badge-red'
return [ 'pt-badge', 'badge-red' ]
case 'waiting':
return 'badge-yellow'
return [ 'pt-badge', 'badge-yellow' ]
}
}
getColspan () {
if (this.jobState === 'all' && this.hasGlobalProgress()) return 7
if (this.jobState === 'all' || this.hasGlobalProgress()) return 6
return 5
return []
}
onJobStateOrTypeChanged () {
@ -174,13 +176,10 @@ export class JobsComponent extends RestTable implements OnInit {
private loadJobStateAndType () {
const state = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_STATE)
if (state) this.jobState = state as JobState
// FIXME: We use <ng-option> that doesn't escape HTML
// https://github.com/ng-select/ng-select/issues/1363
if (state) this.jobState = escapeHTML(state) as JobState
const type = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE)
if (type) this.jobType = type as JobType
const jobType = peertubeLocalStorage.getItem(JobsComponent.LOCAL_STORAGE_TYPE)
if (jobType) this.jobType = jobType as JobType
}
private saveJobStateAndType () {

View File

@ -19,33 +19,21 @@
<div>
<label i18n for="log-start-date">Start date</label>
<ng-select
[(ngModel)]="startDate"
(ngModelChange)="refresh()"
[clearable]="false"
[searchable]="false"
labelForId="log-start-date"
>
<ng-option *ngFor="let time of timeChoices" [value]="time.id">
{{ time.label }} ({{ time.id | date: time.dateFormat }} - <span i18n>now</span>)
</ng-option>
</ng-select>
<my-select-options inputId="log-start-date" [items]="timeChoices" [(ngModel)]="startDate" (ngModelChange)="refresh()">
<ng-template ptTemplate="item" let-item>
{{ item.label }} ({{ item.id | date: item.dateFormat }} - <span i18n>now</span>)
</ng-template>
</my-select-options>
</div>
<div>
<label i18n for="log-level">Log level</label>
<ng-select
[(ngModel)]="level"
(ngModelChange)="refresh()"
[clearable]="false"
[searchable]="false"
labelForId="log-level"
>
<ng-option *ngFor="let levelChoice of levelChoices" [value]="levelChoice.id">
<span class="level-choice me-1" [ngClass]="levelChoice.id">&#11044;</span> {{ levelChoice.label }}
</ng-option>
</ng-select>
<my-select-options inputId="log-level" [items]="levelChoices" [(ngModel)]="level" (ngModelChange)="refresh()">
<ng-template ptTemplate="item" let-item>
<span class="level-choice me-1" [ngClass]="item.id">&#11044;</span> {{ item.label }}
</ng-template>
</my-select-options>
</div>
<div>

View File

@ -6,6 +6,7 @@
font-size: 13px;
max-height: 500px;
overflow-y: auto;
color: #000;
background: rgb(250, 250, 250);
padding: 20px;
@ -67,8 +68,9 @@
}
.level-choice {
font-size: 80%;
vertical-align: text-top;
font-size: 14px;
position: relative;
top: -1px;
&.debug {
color: rgb(197, 197, 197);
@ -83,7 +85,7 @@
}
&.error {
color: #DC262B;
color: pvar(--red);
}
}
@ -97,9 +99,7 @@ my-copy-button {
.header {
flex-direction: column;
.peertube-select-container,
ng-select,
my-button {
> * {
width: 100% !important;
margin-bottom: 10px !important;

View File

@ -2,8 +2,9 @@ import { DatePipe, NgClass, NgFor, NgIf } from '@angular/common'
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { LocalStorageService, Notifier } from '@app/core'
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { NgSelectModule } from '@ng-select/ng-select'
import { PeerTubeTemplateDirective } from '@app/shared/shared-main/common/peertube-template.directive'
import { ServerLogLevel } from '@peertube/peertube-models'
import { SelectTagsComponent } from '../../../shared/shared-forms/select/select-tags.component'
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
@ -18,14 +19,15 @@ import { LogsService } from './logs.service'
imports: [
FormsModule,
NgFor,
NgSelectModule,
NgIf,
NgClass,
SelectTagsComponent,
ButtonComponent,
DatePipe,
CopyButtonComponent,
GlobalIconComponent
GlobalIconComponent,
SelectOptionsComponent,
PeerTubeTemplateDirective
]
})
export class LogsComponent implements OnInit {

View File

@ -10,7 +10,7 @@
<div class="modal-body" [formGroup]="form">
<div class="form-group">
<label i18n for="channel">Select a channel to receive the video</label>
<my-select-channel labelForId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel>
<my-select-channel class="d-block" inputId="channel" formControlName="channel" [items]="videoChannels"></my-select-channel>
<div *ngIf="formErrors.channel" class="form-error" role="alert">{{ formErrors.channel }}</div>
</div>

View File

@ -31,7 +31,7 @@
<div class="form-group">
<label i18n for="videoChannel">Video channel</label>
<my-select-channel labelForId="videoChannel" [items]="userVideoChannels" formControlName="videoChannel"></my-select-channel>
<my-select-channel inputId="videoChannel" [items]="userVideoChannels" formControlName="videoChannel"></my-select-channel>
<div *ngIf="formErrors['videoChannel']" class="form-error" role="alert">
{{ formErrors['videoChannel'] }}

View File

@ -59,11 +59,7 @@
<div class="form-group">
<label i18n for="privacy">Privacy</label>
<div class="peertube-select-container">
<my-select-options
labelForId="privacy" [items]="videoPlaylistPrivacies" formControlName="privacy" [clearable]="false"
></my-select-options>
</div>
<my-select-options inputId="privacy" [items]="videoPlaylistPrivacies" formControlName="privacy"></my-select-options>
<div *ngIf="formErrors.privacy" class="form-error" role="alert">{{ formErrors.privacy }}</div>
</div>
@ -71,7 +67,7 @@
<div class="form-group">
<label for="videoChannelId" i18n>Channel</label>
<my-select-channel labelForId="videoChannelId" [items]="userVideoChannels" formControlName="videoChannelId"></my-select-channel>
<my-select-channel inputId="videoChannelId" [items]="userVideoChannels" formControlName="videoChannelId"></my-select-channel>
<div *ngIf="formErrors['videoChannelId']" class="form-error" role="alert">{{ formErrors['videoChannelId'] }}</div>
</div>

View File

@ -9,7 +9,8 @@ input[type=text] {
@include peertube-select-container(340px);
}
my-select-channel {
my-select-channel,
my-select-options {
display: block;
max-width: 340px;
}

View File

@ -20,11 +20,13 @@
</div>
<div class="stats-with-date">
<div class="overall-stats">
<div class="date-filter-wrapper">
<h2>{{ getViewersStatsTitle() }}</h2>
<my-select-options [(ngModel)]="currentDateFilter" (ngModelChange)="onDateFilterChange()" [items]="dateFilters"></my-select-options>
<div class="overall-stats">
<h2>{{ getViewersStatsTitle() }}</h2>
<div class="date-filter-wrapper">
<label class="visually-hidden" for="date-filter">Filter viewers stats by date</label>
<my-select-options inputId="date-filter" [(ngModel)]="currentDateFilter" (ngModelChange)="onDateFilterChange()" [items]="dateFilters"></my-select-options>
</div>
<div class="cards">

View File

@ -11,12 +11,11 @@
<div class="modal-body">
<label i18n for="language">Language</label>
<div class="peertube-ng-select-container">
<ng-select
labelForId="language" [items]="videoCaptionLanguages" formControlName="language"
bindLabel="label" bindValue="id"
></ng-select>
</div>
<my-select-options
inputId="language" [items]="videoCaptionLanguages" formControlName="language"
clearable="true" filter="true" virtualScroll="true"
></my-select-options>
<div *ngIf="formErrors.language" class="form-error" role="alert">
{{ formErrors.language }}

View File

@ -1,23 +1,23 @@
import { NgIf } from '@angular/common'
import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { ServerService } from '@app/core'
import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
import { VideoCaptionEdit } from '@app/shared/shared-main/video-caption/video-caption-edit.model'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { HTMLServerConfig, VideoConstant } from '@peertube/peertube-models'
import { ReactiveFileComponent } from '../../../../shared/shared-forms/reactive-file.component'
import { NgIf } from '@angular/common'
import { NgSelectModule } from '@ng-select/ng-select'
import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { VideoCaptionEdit } from '@app/shared/shared-main/video-caption/video-caption-edit.model'
@Component({
selector: 'my-video-caption-add-modal',
styleUrls: [ './video-caption-add-modal.component.scss' ],
templateUrl: './video-caption-add-modal.component.html',
standalone: true,
imports: [ FormsModule, ReactiveFormsModule, GlobalIconComponent, NgSelectModule, NgIf, ReactiveFileComponent ]
imports: [ FormsModule, ReactiveFormsModule, GlobalIconComponent, NgIf, ReactiveFileComponent, SelectOptionsComponent ]
})
export class VideoCaptionAddModalComponent extends FormReactive implements OnInit {

View File

@ -64,14 +64,12 @@
<div class="col-video-edit">
<div class="form-group">
<label i18n for="channel">Channel</label>
<my-select-channel labelForId="channel" [items]="userVideoChannels" formControlName="channelId"></my-select-channel>
<my-select-channel class="d-block" inputId="channel" [items]="userVideoChannels" formControlName="channelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="category">Category</label>
<my-select-options
labelForId="category" [items]="videoCategories" formControlName="category" [clearable]="true"
></my-select-options>
<my-select-options inputId="category" [items]="videoCategories" formControlName="category" clearable="true"></my-select-options>
<div *ngIf="formErrors.category" class="form-error" role="alert">
{{ formErrors.category }}
@ -89,9 +87,7 @@
</ng-template>
</my-help>
<my-select-options
labelForId="licence" [items]="videoLicences" formControlName="licence" [clearable]="true"
></my-select-options>
<my-select-options inputId="licence" [items]="videoLicences" formControlName="licence" clearable="true"></my-select-options>
<div *ngIf="formErrors.licence" class="form-error" role="alert">
{{ formErrors.licence }}
@ -101,8 +97,8 @@
<div class="form-group">
<label i18n for="language">Language</label>
<my-select-options
labelForId="language" [items]="videoLanguages" formControlName="language"
[clearable]="true" [searchable]="true" [groupBy]="'group'"
inputId="language" [items]="videoLanguages" formControlName="language"
clearable="true" filter="true" virtualScroll="true"
></my-select-options>
<div *ngIf="formErrors.language" class="form-error" role="alert">
@ -112,9 +108,7 @@
<div class="form-group">
<label i18n for="privacy">Privacy</label>
<my-select-options
labelForId="privacy" [items]="videoPrivacies" formControlName="privacy" [clearable]="false"
></my-select-options>
<my-select-options inputId="privacy" [items]="videoPrivacies" formControlName="privacy"></my-select-options>
<div *ngIf="formErrors.privacy" class="form-error" role="alert">
{{ formErrors.privacy }}
@ -358,16 +352,12 @@
<div class="form-group mx-4" *ngIf="isSaveReplayEnabled()">
<label i18n for="replayPrivacy">Privacy of the new replay</label>
<my-select-options
labelForId="replayPrivacy" [items]="replayPrivacies" [clearable]="false" formControlName="replayPrivacy"
></my-select-options>
<my-select-options inputId="replayPrivacy" [items]="replayPrivacies" formControlName="replayPrivacy"></my-select-options>
</div>
<div class="form-group" *ngIf="isLatencyModeEnabled()">
<label i18n for="latencyMode">Latency mode</label>
<my-select-options
labelForId="latencyMode" [items]="latencyModes" formControlName="latencyMode" [clearable]="true"
></my-select-options>
<my-select-options inputId="latencyMode" [items]="latencyModes" formControlName="latencyMode" clearable="true"></my-select-options>
<div *ngIf="formErrors.latencyMode" class="form-error" role="alert">
{{ formErrors.latencyMode }}
@ -454,7 +444,7 @@
<div class="form-group mb-4">
<label i18n for="commentsPolicy">Comments policy</label>
<my-select-options labelForId="commentsPolicy" [items]="commentPolicies" formControlName="commentsPolicy" [clearable]="false"></my-select-options>
<my-select-options inputId="commentsPolicy" [items]="commentPolicies" formControlName="commentsPolicy"></my-select-options>
<div *ngIf="formErrors.commentsPolicy" class="form-error" role="alert">
{{ formErrors.commentsPolicy }}

View File

@ -82,7 +82,6 @@ import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { ThumbnailManagerComponent } from './thumbnail-manager/thumbnail-manager.component'
import { VideoEditType } from './video-edit.type'
type VideoLanguages = VideoConstant<string> & { group?: string }
type PluginField = {
pluginInfo: PluginInfo
commonOptions: RegisterClientFormFieldOptions
@ -167,7 +166,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
videoCategories: VideoConstant<number>[] = []
videoLicences: VideoConstant<number>[] = []
commentPolicies: VideoConstant<VideoCommentPolicyType>[] = []
videoLanguages: VideoLanguages[] = []
videoLanguages: VideoConstant<string>[] = []
latencyModes: SelectOptionsItem[] = [
{
id: LiveVideoLatencyMode.SMALL_LATENCY,
@ -303,16 +302,14 @@ export class VideoEditComponent implements OnInit, OnDestroy {
this.instanceService.getAbout(),
this.serverService.getVideoLanguages()
]).pipe(map(([ about, languages ]) => ({ about, languages })))
.subscribe(res => {
this.videoLanguages = res.languages
.map(l => {
if (l.id === 'zxx') return { ...l, group: $localize`Other`, groupOrder: 1 }
.subscribe(({ about, languages }) => {
this.videoLanguages = [
...languages.filter(l => about.instance.languages.includes(l.id)),
return res.about.instance.languages.includes(l.id)
? { ...l, group: $localize`Instance languages`, groupOrder: 0 }
: { ...l, group: $localize`All languages`, groupOrder: 2 }
})
.sort((a, b) => a.groupOrder - b.groupOrder)
languages.find(l => l.id === 'zxx'),
...languages.filter(l => !about.instance.languages.includes(l.id) && l.id !== 'zxx')
]
})
this.serverService.getVideoPrivacies()

View File

@ -4,14 +4,12 @@
<div class="form-group">
<label i18n for="first-step-channel">Channel</label>
<my-select-channel labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
<my-select-channel inputId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
<my-select-options
labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
></my-select-options>
<my-select-options inputId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"></my-select-options>
</div>
<div class="form-group live-type">

View File

@ -28,14 +28,12 @@
<div class="form-group">
<label i18n for="first-step-channel">Channel</label>
<my-select-channel labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
<my-select-channel inputId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
<my-select-options
labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
></my-select-options>
<my-select-options inputId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"></my-select-options>
</div>
<input

View File

@ -24,14 +24,12 @@
<div class="form-group">
<label i18n for="first-step-channel">Channel</label>
<my-select-channel labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
<my-select-channel inputId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
<my-select-options
labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
></my-select-options>
<my-select-options inputId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"></my-select-options>
</div>
<input

View File

@ -26,10 +26,12 @@ $width-size: 275px;
.peertube-select-container {
@include peertube-select-container($width-size);
}
my-select-options ::ng-deep ng-select,
my-select-channel ::ng-deep ng-select,
my-select-channel,
my-select-options,
.peertube-radio-container,
.form-group-description {
display: block;
width: $width-size;
@media screen and (max-width: $width-size) {

View File

@ -17,14 +17,12 @@
<div class="form-group form-group-channel">
<label i18n for="first-step-channel">Channel</label>
<my-select-channel labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
<my-select-channel inputId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
<my-select-options
labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
></my-select-options>
<my-select-options inputId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"></my-select-options>
</div>
<ng-container *ngIf="isUploadingAudioFile">

View File

@ -30,8 +30,8 @@
<label i18n for="transcription-language">Language</label>
<my-select-options
labelForId="transcription-language" [items]="languagesOptions"
[(ngModel)]="currentLanguage" (ngModelChange)="updateCurrentCaption()" clearable="false"
inputId="transcription-language" [items]="languagesOptions"
[(ngModel)]="currentLanguage" (ngModelChange)="updateCurrentCaption()"
></my-select-options>
</div>
</div>

View File

@ -13,7 +13,8 @@ export class HotkeysService {
cheatSheetToggle = new Subject<boolean>()
private hotkeys: Hotkey[] = []
private readonly preventIn = new Set([ 'INPUT', 'SELECT', 'TEXTAREA' ])
private readonly preventInNode = new Set([ 'INPUT', 'SELECT', 'TEXTAREA' ])
private readonly preventInRole = new Set([ 'combobox' ])
private disabled = false
@ -62,7 +63,7 @@ export class HotkeysService {
const target = event.target as HTMLElement
const nodeName: string = target.nodeName.toUpperCase()
if (target.isContentEditable || this.preventIn.has(nodeName)) {
if (target.isContentEditable || this.preventInNode.has(nodeName) || this.preventInRole.has(target.getAttribute('role'))) {
return
}

View File

@ -1,9 +0,0 @@
<my-select-checkbox-all
[labelForId]="labelForId"
[(ngModel)]="selectedCategories"
(ngModelChange)="onModelChange()"
[availableItems]="availableCategories"
i18n-placeholder placeholder="Add a new category"
[allGroupLabel]="allCategoriesGroup"
>
</my-select-checkbox-all>

View File

@ -1,14 +1,25 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { ServerService } from '@app/core'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { ItemSelectCheckboxValue } from './select-checkbox.component'
import { SelectCheckboxAllComponent } from './select-checkbox-all.component'
import { SelectCheckboxDefaultAllComponent } from './select-checkbox-default-all.component'
import { NgIf } from '@angular/common'
@Component({
selector: 'my-select-categories',
styleUrls: [ './select-shared.component.scss' ],
templateUrl: './select-categories.component.html',
template: `
<my-select-checkbox-default-all
*ngIf="availableCategories"
[inputId]="inputId"
[(ngModel)]="selectedCategories"
(ngModelChange)="onModelChange()"
[availableItems]="availableCategories"
i18n-placeholder placeholder="Add a new category"
i18n-allSelectedLabel allSelectedLabel="All categories"
i18n-selectedLabel selectedLabel="{1} categories selected"
>
</my-select-checkbox-default-all>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -17,19 +28,13 @@ import { SelectCheckboxAllComponent } from './select-checkbox-all.component'
}
],
standalone: true,
imports: [ SelectCheckboxAllComponent, FormsModule ]
imports: [ SelectCheckboxDefaultAllComponent, FormsModule, NgIf ]
})
export class SelectCategoriesComponent implements ControlValueAccessor, OnInit {
@Input({ required: true }) labelForId: string
@Input({ required: true }) inputId: string
selectedCategories: ItemSelectCheckboxValue[] = []
availableCategories: SelectOptionsItem[] = []
allCategoriesGroup = $localize`All categories`
// Fix a bug on ng-select when we update items after we selected items
private toWrite: any
private loaded = false
selectedCategories: string[]
availableCategories: SelectOptionsItem[]
constructor (
private server: ServerService
@ -41,9 +46,7 @@ export class SelectCategoriesComponent implements ControlValueAccessor, OnInit {
this.server.getVideoCategories()
.subscribe(
categories => {
this.availableCategories = categories.map(c => ({ label: c.label, id: c.id + '', group: this.allCategoriesGroup }))
this.loaded = true
this.writeValue(this.toWrite)
this.availableCategories = categories.map(c => ({ label: c.label, id: c.id + '' }))
}
)
}
@ -51,14 +54,9 @@ export class SelectCategoriesComponent implements ControlValueAccessor, OnInit {
propagateChange = (_: any) => { /* empty */ }
writeValue (categories: string[] | number[]) {
if (!this.loaded) {
this.toWrite = categories
return
}
this.selectedCategories = categories
? categories.map(c => c + '')
: categories as string[]
: null
}
registerOnChange (fn: (_: any) => void) {
@ -70,6 +68,10 @@ export class SelectCategoriesComponent implements ControlValueAccessor, OnInit {
}
onModelChange () {
this.propagateChange(this.selectedCategories)
this.propagateChange(
this.selectedCategories
? this.selectedCategories.map(c => c + '')
: null
)
}
}

View File

@ -1,14 +0,0 @@
<ng-select
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"
[bindLabel]="bindLabel"
[bindValue]="bindValue"
[clearable]="clearable"
[searchable]="searchable"
[labelForId]="labelForId"
>
<ng-option *ngFor="let channel of channels" [value]="channel.id">
<img alt="" class="avatar me-1" [src]="channel.avatarPath" />
{{ channel.label }}
</ng-option>
</ng-select>

View File

@ -1,14 +1,25 @@
import { CommonModule } from '@angular/common'
import { Component, forwardRef, Input, OnChanges } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { SelectChannelItem } from '../../../../types/select-options-item.model'
import { NgFor } from '@angular/common'
import { NgSelectModule } from '@ng-select/ng-select'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
import { DropdownModule } from 'primeng/dropdown'
import { SelectChannelItem, SelectOptionsItem } from '../../../../types/select-options-item.model'
import { SelectOptionsComponent } from './select-options.component'
@Component({
selector: 'my-select-channel',
styleUrls: [ './select-shared.component.scss' ],
templateUrl: './select-channel.component.html',
template: `
<my-select-options
[inputId]="inputId"
[items]="channels"
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"
[filter]="channels && channels.length > 5"
></my-select-options>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -17,28 +28,22 @@ import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.mode
}
],
standalone: true,
imports: [ NgSelectModule, FormsModule, NgFor ]
imports: [ DropdownModule, FormsModule, CommonModule, SelectOptionsComponent ]
})
export class SelectChannelComponent implements ControlValueAccessor, OnChanges {
@Input({ required: true }) labelForId: string
@Input({ required: true }) inputId: string
@Input() items: SelectChannelItem[] = []
channels: SelectChannelItem[] = []
channels: SelectOptionsItem[]
selectedId: number
// ng-select options
bindLabel = 'label'
bindValue = 'id'
clearable = false
searchable = false
ngOnChanges () {
this.channels = this.items.map(c => {
const avatarPath = c.avatarPath
? c.avatarPath
: VideoChannel.GET_DEFAULT_AVATAR_URL(20)
: VideoChannel.GET_DEFAULT_AVATAR_URL(21)
return Object.assign({}, c, { avatarPath })
return Object.assign({}, c, { imageUrl: avatarPath })
})
}
@ -61,4 +66,8 @@ export class SelectChannelComponent implements ControlValueAccessor, OnChanges {
onModelChange () {
this.propagateChange(this.selectedId)
}
getSelectedChannel () {
return (this.channels || []).find(c => c.id + '' === this.selectedId + '')
}
}

View File

@ -1,123 +0,0 @@
import { Component, forwardRef, Input } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { Notifier } from '@app/core'
import { formatICU } from '@app/helpers'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { ItemSelectCheckboxValue, SelectCheckboxComponent } from './select-checkbox.component'
@Component({
selector: 'my-select-checkbox-all',
styleUrls: [ './select-shared.component.scss' ],
template: `
<my-select-checkbox
[(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()"
[availableItems]="availableItems"
[selectableGroup]="true" [selectableGroupAsModel]="true"
[placeholder]="placeholder"
[labelForId]="labelForId"
(focusout)="onBlur()"
>
</my-select-checkbox>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectCheckboxAllComponent),
multi: true
}
],
standalone: true,
imports: [ SelectCheckboxComponent, FormsModule ]
})
export class SelectCheckboxAllComponent implements ControlValueAccessor {
@Input({ required: true }) labelForId: string
@Input() availableItems: SelectOptionsItem[] = []
@Input() allGroupLabel: string
@Input() placeholder: string
@Input() maxItems: number
selectedItems: ItemSelectCheckboxValue[]
constructor (
private notifier: Notifier
) {
}
propagateChange = (_: any) => { /* empty */ }
writeValue (items: string[]) {
this.selectedItems = items
? items.map(l => ({ id: l }))
: [ { group: this.allGroupLabel } ]
}
registerOnChange (fn: (_: any) => void) {
this.propagateChange = fn
}
registerOnTouched () {
// Unused
}
onModelChange () {
if (!this.isMaxConstraintValid()) return
this.propagateChange(this.buildOutputItems())
}
onBlur () {
// Automatically use "All languages" if the user did not select any language
if (Array.isArray(this.selectedItems) && this.selectedItems.length === 0) {
this.selectedItems = [ { group: this.allGroupLabel } ]
}
}
private isMaxConstraintValid () {
if (!this.maxItems) return true
const outputItems = this.buildOutputItems()
if (!outputItems) return true
if (outputItems.length >= this.maxItems) {
this.notifier.error(
formatICU(
$localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`,
{ maxItems: this.maxItems }
)
)
return false
}
return true
}
private buildOutputItems () {
if (!Array.isArray(this.selectedItems)) return undefined
// null means "All"
if (this.selectedItems.length === 0 || this.selectedItems.length === this.availableItems.length) {
return null
}
if (this.selectedItems.length === 1) {
const item = this.selectedItems[0]
const itemGroup = typeof item === 'string' || typeof item === 'number'
? item
: item.group
if (itemGroup === this.allGroupLabel) return null
}
return this.selectedItems.map(l => {
if (typeof l === 'string' || typeof l === 'number') return l
if (l.group) return l.group
return l.id + ''
})
}
}

View File

@ -0,0 +1,136 @@
import { booleanAttribute, Component, forwardRef, Input } from '@angular/core'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { Notifier } from '@app/core'
import { formatICU } from '@app/helpers'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { SelectCheckboxComponent } from './select-checkbox.component'
@Component({
selector: 'my-select-checkbox-default-all',
template: `
<my-select-checkbox
[(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()"
[availableItems]="availableItems"
[placeholder]="placeholder"
[inputId]="inputId"
[selectedItemsLabel]="selectedItemsLabel"
showClear="false"
[virtualScroll]="virtualScroll"
(panelHide)="onPanelHide()"
>
</my-select-checkbox>`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SelectCheckboxDefaultAllComponent),
multi: true
}
],
standalone: true,
imports: [ SelectCheckboxComponent, FormsModule ]
})
export class SelectCheckboxDefaultAllComponent implements ControlValueAccessor {
@Input({ required: true }) inputId: string
@Input() availableItems: SelectOptionsItem[] = []
@Input() placeholder: string
@Input() maxIndividualItems: number
@Input() allSelectedLabel: string
@Input() selectedLabel: string
@Input({ transform: booleanAttribute }) virtualScroll = false
selectedItemsLabel: string
selectedItems: string[]
constructor (private notifier: Notifier) {
}
propagateChange = (_: any) => { /* empty */ }
writeValue (items: string[]) {
if (items) this.selectedItems = items
else this.selectAll()
}
registerOnChange (fn: (_: any) => void) {
this.propagateChange = fn
}
registerOnTouched () {
// Unused
}
onModelChange () {
this.updateLabel()
if (!this.isMaxItemsValid()) return
this.propagateChange(this.buildOutputItems())
}
onPanelHide () {
// Automatically use "All languages" if the user did not select any language
if (Array.isArray(this.selectedItems) && this.selectedItems.length === 0) {
this.selectAll()
}
this.checkMaxItems()
}
private isMaxItemsValid () {
if (!this.maxIndividualItems) return true
const outputItems = this.buildOutputItems()
if (!outputItems) return true
if (outputItems.length >= this.maxIndividualItems) return false
return true
}
private checkMaxItems () {
if (!this.isMaxItemsValid()) {
this.notifier.error(
formatICU(
$localize`You can't select more than {maxItems, plural, =1 {1 item} other {{maxItems} items}}`,
{ maxItems: this.maxIndividualItems }
)
)
this.selectAll()
}
}
private selectAll () {
this.selectedItems = this.availableItems.map(i => i.id + '')
this.updateLabel()
}
private updateLabel () {
if (this.selectedItems && this.availableItems && this.selectedItems.length === this.availableItems.length) {
this.selectedItemsLabel = this.allSelectedLabel
} else {
this.selectedItemsLabel = this.selectedLabel
}
}
private buildOutputItems () {
if (!Array.isArray(this.selectedItems)) return undefined
// null means "All"
if (this.selectedItems.length === 0 || this.selectedItems.length === this.availableItems.length) {
return null
}
return this.selectedItems
}
}

View File

@ -1,41 +1,22 @@
<ng-select
[items]="availableItems"
<p-multiSelect
[inputId]="inputId"
[options]="availableItems"
[(ngModel)]="selectedItems"
(ngModelChange)="onModelChange()"
[placeholder]="placeholder"
[clearable]="true"
[multiple]="true"
[searchable]="true"
[closeOnSelect]="false"
[disabled]="disabled"
[labelForId]="labelForId"
bindValue="id"
bindLabel="label"
[showClear]="showClear"
notFoundText="No items found" i18n-notFoundText
optionValue="id"
[selectableGroup]="selectableGroup"
[selectableGroupAsModel]="selectableGroupAsModel"
[selectedItemsLabel]="selectedItemsLabel"
[selectionLimit]="selectionLimit"
groupBy="group"
[compareWith]="compareFn"
[virtualScroll]="virtualScroll"
[virtualScrollItemSize]="virtualScrollItemSize"
(onPanelHide)="panelHide.emit()"
>
<ng-template ng-optgroup-tmp let-item="item" let-item$="item$" let-index="index">
<div class="checkbox-wrapper">
<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="checkbox-wrapper">
<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>
</p-multiSelect>

View File

@ -1,18 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
ng-select ::ng-deep {
.ng-option {
display: flex;
align-items: center;
}
.checkbox-wrapper {
display: flex;
align-items: center;
input {
@include peertube-checkbox(1px);
}
}
}

View File

@ -1,13 +1,11 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { CommonModule } from '@angular/common'
import { booleanAttribute, Component, EventEmitter, forwardRef, Input, numberAttribute, Output } from '@angular/core'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { MultiSelectModule } from 'primeng/multiselect'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { NgSelectModule } from '@ng-select/ng-select'
export type ItemSelectCheckboxValue = { id?: string, group?: string } | string
@Component({
selector: 'my-select-checkbox',
styleUrls: [ './select-shared.component.scss', 'select-checkbox.component.scss' ],
templateUrl: './select-checkbox.component.html',
providers: [
{
@ -17,40 +15,35 @@ export type ItemSelectCheckboxValue = { id?: string, group?: string } | string
}
],
standalone: true,
imports: [ NgSelectModule, FormsModule ]
imports: [ MultiSelectModule, FormsModule, CommonModule ]
})
export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
@Input({ required: true }) labelForId: string
export class SelectCheckboxComponent implements ControlValueAccessor {
@Input({ required: true }) inputId: string
@Input() availableItems: SelectOptionsItem[] = []
@Input() selectedItems: ItemSelectCheckboxValue[] = []
@Input() selectedItems: string[] = []
@Input() selectableGroup: boolean
@Input() selectableGroupAsModel: boolean
@Input() placeholder: string
disabled = false
@Input() selectionLimit: number
ngOnInit () {
if (!this.placeholder) this.placeholder = $localize`Add a new option`
}
@Input() selectedItemsLabel: string
@Input({ transform: booleanAttribute }) virtualScroll = false
@Input({ transform: numberAttribute }) virtualScrollItemSize = 33
@Input({ transform: booleanAttribute }) showClear: boolean
@Output() panelHide = new EventEmitter()
disabled = false
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
}
writeValue (items: string[]) {
this.selectedItems = items
}
registerOnChange (fn: (_: any) => void) {
@ -68,20 +61,4 @@ export class SelectCheckboxComponent implements OnInit, ControlValueAccessor {
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
compareFn (item: SelectOptionsItem, selected: ItemSelectCheckboxValue) {
if (typeof selected === 'string' || typeof selected === 'number') {
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
}
}

View File

@ -1,19 +1,21 @@
<div class="root">
<div class="d-flex align-items-center">
<my-select-options
[items]="itemsWithCustom"
[clearable]="clearable"
[searchable]="searchable"
[groupBy]="groupBy"
[labelForId]="labelForId"
class="flex-grow-1"
[inputId]="inputId"
[disabled]="disabled"
[items]="itemsWithCustom"
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"
></my-select-options>
<ng-container *ngIf="isCustomValue()">
<input [attr.aria-labelledby]="labelId" [(ngModel)]="customValue" (ngModelChange)="onModelChange()" [type]="inputType" class="form-control" />
@if (isCustomValue()) {
<input
[attr.aria-labelledby]="labelId" [(ngModel)]="customValue" (ngModelChange)="onModelChange()"
[type]="inputType" class="ms-2 form-control pt-input-text"
/>
<span *ngIf="inputSuffix" class="input-suffix">{{ inputSuffix }}</span>
</ng-container>
<span *ngIf="inputSuffix" class="ms-1">{{ inputSuffix }}</span>
}
</div>

View File

@ -6,7 +6,6 @@ import { SelectOptionsComponent } from './select-options.component'
@Component({
selector: 'my-select-custom-value',
styleUrls: [ './select-shared.component.scss' ],
templateUrl: './select-custom-value.component.html',
providers: [
{
@ -19,14 +18,14 @@ import { SelectOptionsComponent } from './select-options.component'
imports: [ SelectOptionsComponent, FormsModule, NgIf ]
})
export class SelectCustomValueComponent implements ControlValueAccessor, OnChanges {
@Input({ required: true }) labelForId: string
@Input({ required: true }) inputId: string
@Input({ required: true }) labelId: string
@Input() items: SelectOptionsItem[] = []
@Input() clearable = false
@Input() searchable = false
@Input() groupBy: string
@Input() inputSuffix: string
@Input() inputType = 'text'

View File

@ -1,10 +0,0 @@
<my-select-checkbox-all
[(ngModel)]="selectedLanguages"
(ngModelChange)="onModelChange()"
[availableItems]="availableLanguages"
[maxItems]="maxLanguages"
i18n-placeholder placeholder="Add a new language"
[allGroupLabel]="allLanguagesGroup"
[labelForId]="labelForId"
>
</my-select-checkbox-all>

View File

@ -1,14 +1,33 @@
import { NgIf } from '@angular/common'
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { ServerService } from '@app/core'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { ItemSelectCheckboxValue } from './select-checkbox.component'
import { SelectCheckboxAllComponent } from './select-checkbox-all.component'
import { SelectCheckboxDefaultAllComponent } from './select-checkbox-default-all.component'
@Component({
selector: 'my-select-languages',
styleUrls: [ './select-shared.component.scss' ],
templateUrl: './select-languages.component.html',
template: `
<my-select-checkbox-default-all
*ngIf="availableLanguages"
[availableItems]="availableLanguages"
[(ngModel)]="selectedLanguages"
(ngModelChange)="onModelChange()"
[inputId]="inputId"
[maxIndividualItems]="maxLanguages"
virtualScroll="true"
virtualScrollItemSize="37"
i18n-allSelectedLabel allSelectedLabel="All languages"
i18n-selectedLabel selectedLabel="{1} languages selected"
i18n-placeholder placeholder="Add a new language"
>
</my-select-checkbox-default-all>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -17,20 +36,14 @@ import { SelectCheckboxAllComponent } from './select-checkbox-all.component'
}
],
standalone: true,
imports: [ SelectCheckboxAllComponent, FormsModule ]
imports: [ SelectCheckboxDefaultAllComponent, FormsModule, NgIf ]
})
export class SelectLanguagesComponent implements ControlValueAccessor, OnInit {
@Input({ required: true }) labelForId: string
@Input({ required: true }) inputId: string
@Input() maxLanguages: number
selectedLanguages: ItemSelectCheckboxValue[]
availableLanguages: (SelectOptionsItem & { groupOrder: number })[] = []
allLanguagesGroup = $localize`All languages`
// Fix a bug on ng-select when we update items after we selected items
private toWrite: any
private loaded = false
selectedLanguages: string[]
availableLanguages: SelectOptionsItem[]
constructor (
private server: ServerService
@ -42,35 +55,27 @@ export class SelectLanguagesComponent implements ControlValueAccessor, OnInit {
this.server.getVideoLanguages()
.subscribe(
languages => {
this.availableLanguages = [ {
label: $localize`Unknown language`,
id: '_unknown',
group: this.allLanguagesGroup,
groupOrder: 1
} ]
const noLangSet = languages.find(l => l.id === 'zxx')
this.availableLanguages = this.availableLanguages
.concat(languages.map(l => {
if (l.id === 'zxx') return { label: l.label, id: l.id, group: $localize`Other`, groupOrder: 0 }
return { label: l.label, id: l.id, group: this.allLanguagesGroup, groupOrder: 1 }
}))
this.availableLanguages = [
{
label: $localize`Unknown language`,
id: '_unknown'
},
this.availableLanguages.sort((a, b) => a.groupOrder - b.groupOrder)
noLangSet,
this.loaded = true
this.writeValue(this.toWrite)
...languages
.filter(l => l.id !== 'zxx')
.map(l => ({ label: l.label, id: l.id }))
]
}
)
}
propagateChange = (_: any) => { /* empty */ }
writeValue (languages: ItemSelectCheckboxValue[]) {
if (!this.loaded) {
this.toWrite = languages
return
}
writeValue (languages: string[]) {
this.selectedLanguages = languages
}

View File

@ -1,22 +1,44 @@
<ng-select
[items]="items"
[groupBy]="groupBy"
<p-dropdown
[inputId]="inputId"
[options]="items"
[(ngModel)]="selectedId"
(ngModelChange)="onModelChange()"
[clearable]="clearable"
[labelForId]="labelForId"
[searchable]="searchable"
[searchFn]="searchFn"
[disabled]="disabled"
bindLabel="label"
bindValue="id"
optionValue="id"
[showClear]="clearable"
[filter]="filter"
filterBy="label"
i18n-filterPlaceholder filterPlaceholder="Search"
i18n-emptyFilterMessage emptyFilterMessage="No results found"
i18n-emptyMessage emptyMessage="No items available"
[virtualScroll]="virtualScroll"
[virtualScrollItemSize]="virtualScrollItemSize"
>
<ng-template ng-option-tmp let-item="item" let-index="index">
{{ item.label }}
<ng-container *ngIf="item.description">
<br>
<span [title]="item.description" class="muted">{{ item.description }}</span>
</ng-container>
<ng-template #itemTemplate let-item let-description="description">
@if (customItemTemplate) {
<ng-template [ngTemplateOutlet]="customItemTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
} @else {
<div class="d-flex align-items-center">
<img *ngIf="item.imageUrl" alt="" class="me-2" [src]="item.imageUrl" />
<span [ngClass]="item.classes">{{ item.label }}</span>
</div>
<span *ngIf="description" class="muted">{{ description }}</span>
}
</ng-template>
</ng-select>
<ng-template pTemplate="selectedItem">
<ng-template [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: getSelectedItem() }"></ng-template>
</ng-template>
<ng-template let-item pTemplate="item">
<ng-template [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, description: item.description }"></ng-template>
</ng-template>
</p-dropdown>

View File

@ -0,0 +1,13 @@
@use '_variables' as *;
@use '_mixins' as *;
img {
border-radius: 50%;
height: 21px;
width: 21px;
margin-top: -1px;
}
.muted {
font-size: 90%;
}

View File

@ -1,13 +1,28 @@
import { booleanAttribute, Component, forwardRef, HostListener, Input } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'
import { CommonModule } from '@angular/common'
import {
AfterContentInit,
booleanAttribute,
ChangeDetectorRef,
Component,
ContentChildren,
forwardRef,
HostListener,
Input,
numberAttribute,
QueryList,
TemplateRef
} from '@angular/core'
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
import { PeerTubeTemplateDirective } from '@app/shared/shared-main/common/peertube-template.directive'
import { DropdownModule } from 'primeng/dropdown'
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
import { NgIf } from '@angular/common'
import { NgSelectModule } from '@ng-select/ng-select'
@Component({
selector: 'my-select-options',
styleUrls: [ './select-shared.component.scss' ],
templateUrl: './select-options.component.html',
styleUrls: [ './select-options.component.scss' ],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -16,22 +31,39 @@ import { NgSelectModule } from '@ng-select/ng-select'
}
],
standalone: true,
imports: [ NgSelectModule, FormsModule, NgIf ]
imports: [ DropdownModule, FormsModule, CommonModule ]
})
export class SelectOptionsComponent implements ControlValueAccessor {
export class SelectOptionsComponent implements AfterContentInit, ControlValueAccessor {
@Input() items: SelectOptionsItem[] = []
@Input({ required: true }) inputId: string
@Input({ transform: booleanAttribute }) clearable = false
@Input({ transform: booleanAttribute }) searchable = false
@Input({ transform: booleanAttribute }) filter = false
@Input() groupBy: string
@Input() labelForId: string
@Input({ transform: booleanAttribute }) virtualScroll = false
@Input({ transform: numberAttribute }) virtualScrollItemSize = 39
@Input() searchFn: any
@ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'item'>>
customItemTemplate: TemplateRef<any>
selectedId: number | string
disabled = false
wroteValue: number | string
constructor (private cd: ChangeDetectorRef) {
}
ngAfterContentInit () {
{
const t = this.templates.find(t => t.name === 'item')
if (t) this.customItemTemplate = t.template
}
}
propagateChange = (_: any) => { /* empty */ }
// Allow plugins to update our value
@ -43,6 +75,10 @@ export class SelectOptionsComponent implements ControlValueAccessor {
writeValue (id: number | string) {
this.selectedId = id
// https://github.com/primefaces/primeng/issues/14609 workaround
this.wroteValue = id
this.cd.detectChanges()
}
registerOnChange (fn: (_: any) => void) {
@ -54,10 +90,18 @@ export class SelectOptionsComponent implements ControlValueAccessor {
}
onModelChange () {
if (this.wroteValue !== undefined && this.wroteValue === this.selectedId) {
return
}
this.propagateChange(this.selectedId)
}
setDisabledState (isDisabled: boolean) {
this.disabled = isDisabled
}
getSelectedItem () {
return this.items.find(i => i.id === this.selectedId)
}
}

View File

@ -1,56 +0,0 @@
@use '_variables' as *;
@use '_mixins' as *;
$form-base-input-width: auto;
.muted {
font-size: 90%;
}
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;
}
}
.root {
display: flex;
align-items: center;
> my-select-options {
flex-grow: 1;
}
}
my-select-options + input {
display: block;
@include peertube-input-text($form-base-input-width);
@include margin-left(5px);
}
.input-suffix {
@include margin-left(5px);
}

View File

@ -25,7 +25,6 @@ export class SelectTagsComponent implements ControlValueAccessor {
writeValue (items: string[]) {
this.selectedItems = items
this.propagateChange(this.selectedItems)
}
registerOnChange (fn: (_: any) => void) {

View File

@ -30,7 +30,7 @@
</my-help>
<div>
<my-select-languages labelForId="videoLanguages" [maxLanguages]="20" formControlName="videoLanguages"></my-select-languages>
<my-select-languages inputId="videoLanguages" [maxLanguages]="20" formControlName="videoLanguages"></my-select-languages>
</div>
</div>

View File

@ -47,34 +47,11 @@
<label class="visually-hidden" for="sort-videos">Sort videos</label>
<ng-select
class="sort"
formControlName="sort"
labelForId="sort-videos"
[clearable]="false"
[searchable]="false"
[bindValue]="null"
>
<ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option>
<ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option>
<ng-option i18n value="name">Sort by <strong>"Name"</strong></ng-option>
@if (isTrendingSortEnabled('most-viewed')) {
<ng-option i18n value="-trending">Sort by <strong>"Recent Views"</strong></ng-option>
}
@if (isTrendingSortEnabled('hot')) {
<ng-option i18n value="-hot">Sort by <strong>"Hot"</strong></ng-option>
}
@if (isTrendingSortEnabled('most-liked')) {
<ng-option i18n value="-likes">Sort by <strong>"Likes"</strong></ng-option>
}
<ng-option i18n value="-views">Sort by <strong>"Global Views"</strong></ng-option>
</ng-select>
<my-select-options inputId="sort-videos" class="sort" formControlName="sort" [items]="sortItems">
<ng-template ptTemplate="item" let-item>
<ng-container>Sort by <strong>"{{ item.label }}"</strong></ng-container>
</ng-template>
</my-select-options>
</div>
<div [ngbCollapse]="areFiltersCollapsed" [animation]="true">
@ -83,7 +60,7 @@
<label class="with-description" for="languageOneOf" i18n>Languages:</label>
<ng-template *ngTemplateOutlet="updateSettings; context: { $implicit: 'video-languages-subtitles' }"></ng-template>
<my-select-languages labelForId="languageOneOf" [maxLanguages]="20" formControlName="languageOneOf"></my-select-languages>
<my-select-languages inputId="languageOneOf" [maxLanguages]="20" formControlName="languageOneOf"></my-select-languages>
</div>
<div class="form-group" role="radiogroup">
@ -137,7 +114,7 @@
<div class="form-group">
<label for="categoryOneOf" i18n>Categories:</label>
<my-select-categories labelForId="categoryOneOf" formControlName="categoryOneOf"></my-select-categories>
<my-select-categories inputId="categoryOneOf" formControlName="categoryOneOf"></my-select-categories>
</div>
<div class="form-group" *ngIf="canSeeAllVideos()">

View File

@ -111,16 +111,6 @@
min-width: 250px;
max-width: 300px;
height: min-content;
::ng-deep {
.ng-select-container {
height: 33px !important;
}
.ng-value strong {
@include margin-left(5px);
}
}
}
my-select-languages,

View File

@ -5,14 +5,16 @@ import { RouterLink } from '@angular/router'
import { AuthService } from '@app/core'
import { ServerService } from '@app/core/server/server.service'
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { UserRight, VideoConstant } from '@peertube/peertube-models'
import debug from 'debug'
import { Subscription } from 'rxjs'
import { SelectOptionsItem } from 'src/types'
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
import { SelectCategoriesComponent } from '../shared-forms/select/select-categories.component'
import { SelectLanguagesComponent } from '../shared-forms/select/select-languages.component'
import { SelectOptionsComponent } from '../shared-forms/select/select-options.component'
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
import { PeerTubeTemplateDirective } from '../shared-main/common/peertube-template.directive'
import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
import { VideoFilterActive, VideoFilters } from './video-filters.model'
@ -31,12 +33,13 @@ const debugLogger = debug('peertube:videos:VideoFiltersHeaderComponent')
NgIf,
GlobalIconComponent,
NgFor,
NgSelectModule,
NgbCollapse,
NgTemplateOutlet,
SelectLanguagesComponent,
SelectCategoriesComponent,
PeertubeCheckboxComponent
PeertubeCheckboxComponent,
SelectOptionsComponent,
PeerTubeTemplateDirective
]
})
export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
@ -50,6 +53,8 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
form: FormGroup
sortItems: SelectOptionsItem[] = []
private videoCategories: VideoConstant<number>[] = []
private videoLanguages: VideoConstant<string>[] = []
@ -92,6 +97,8 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
this.serverService.getVideoLanguages()
.subscribe(languages => this.videoLanguages = languages)
this.buildSortItems()
}
ngOnDestroy () {
@ -105,7 +112,29 @@ export class VideoFiltersHeaderComponent implements OnInit, OnDestroy {
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
}
isTrendingSortEnabled (sort: 'most-viewed' | 'hot' | 'most-liked') {
private buildSortItems () {
this.sortItems = [
{ id: '-publishedAt', label: 'Recently Added' },
{ id: '-originallyPublishedAt', label: 'Original Publication Date' },
{ id: 'name', label: 'Name' }
]
if (this.isTrendingSortEnabled('most-viewed')) {
this.sortItems.push({ id: '-trending', label: 'Recent Views' })
}
if (this.isTrendingSortEnabled('hot')) {
this.sortItems.push({ id: '-hot', label: 'Hot' })
}
if (this.isTrendingSortEnabled('most-liked')) {
this.sortItems.push({ id: '-likes', label: 'Likes' })
}
this.sortItems.push({ id: '-views', label: 'Global Views' })
}
private isTrendingSortEnabled (sort: 'most-viewed' | 'hot' | 'most-liked') {
const serverConfig = this.serverService.getHTMLConfig()
return serverConfig.trending.videos.algorithms.enabled.includes(sort)

View File

@ -1,5 +1,5 @@
import { splitIntoArray, toBoolean } from '@app/helpers'
import { escapeHTML, getAllPrivacies } from '@peertube/peertube-core-utils'
import { getAllPrivacies } from '@peertube/peertube-core-utils'
import {
BooleanBothQuery,
NSFWPolicyType,
@ -112,28 +112,19 @@ export class VideoFilters {
// ---------------------------------------------------------------------------
load (obj: Partial<AttributesOnly<VideoFilters>>) {
// FIXME: We may use <ng-option> that doesn't escape HTML so prefer to escape things
// https://github.com/ng-select/ng-select/issues/1363
if (obj.sort !== undefined) this.sort = obj.sort
const escapeIfNeeded = (value: any) => {
if (typeof value === 'string') return escapeHTML(value)
if (obj.nsfw !== undefined) this.nsfw = obj.nsfw
return value
}
if (obj.languageOneOf !== undefined) this.languageOneOf = splitIntoArray(obj.languageOneOf)
if (obj.categoryOneOf !== undefined) this.categoryOneOf = splitIntoArray(obj.categoryOneOf)
if (obj.sort !== undefined) this.sort = escapeIfNeeded(obj.sort) as VideoSortField
if (obj.nsfw !== undefined) this.nsfw = escapeIfNeeded(obj.nsfw) as BooleanBothQuery
if (obj.languageOneOf !== undefined) this.languageOneOf = splitIntoArray(escapeIfNeeded(obj.languageOneOf))
if (obj.categoryOneOf !== undefined) this.categoryOneOf = splitIntoArray(escapeIfNeeded(obj.categoryOneOf))
if (obj.scope !== undefined) this.scope = escapeIfNeeded(obj.scope) as VideoFilterScope
if (obj.scope !== undefined) this.scope = obj.scope
if (obj.allVideos !== undefined) this.allVideos = toBoolean(obj.allVideos)
if (obj.live !== undefined) this.live = escapeIfNeeded(obj.live) as BooleanBothQuery
if (obj.live !== undefined) this.live = obj.live
if (obj.search !== undefined) this.search = escapeIfNeeded(obj.search)
if (obj.search !== undefined) this.search = obj.search
this.buildActiveFilters()
}

View File

@ -6,7 +6,6 @@
@use '_icons' as *;
@use '_fonts';
@use './custom-markup';
@use './ng-select';
@use './bootstrap';
@use './primeng-custom';
@use './z-index';

View File

@ -1,6 +1,10 @@
@use '_variables' as *;
@use '_mixins' as *;
.pt-input-text {
@include peertube-input-text(100%);
}
.form-group {
margin-bottom: 1rem;
}

View File

@ -1,99 +0,0 @@
@use 'sass:math';
@use 'sass:color';
@use '_variables' as *;
@use '_mixins' as *;
$ng-select-highlight: pvar(--mainColor);
$ng-select-primary-text: pvar(--mainForegroundColor);
// $ng-select-disabled-text: #f9f9f9 !default;
$ng-select-border: $input-border-color;
// $ng-select-border-radius: 4px !default;
$ng-select-bg: pvar(--mainBackgroundColor);
// Cannot use a CSS variable as the default them use darken on this variable
$ng-select-selected: color.adjust($main-color, $lightness: 40%);
// $ng-select-selected-text: $ng-select-primary-text !default;
$ng-select-marked: pvar(--mainColorVeryLight);
// $ng-select-marked-text: $ng-select-primary-text !default;
$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
$ng-select-placeholder: pvar(--greyForegroundColor);
$ng-select-height: 30px;
$ng-select-value-padding-left: 15px;
$ng-select-value-font-size: $form-input-font-size;
// $ng-select-value-text: $ng-select-primary-text !default;
// $ng-select-dropdown-bg: $ng-select-bg !default;
// $ng-select-dropdown-border: $ng-select-border !default;
// $ng-select-dropdown-optgroup-text: rgba(0, 0, 0, 0.54) !default;
// $ng-select-dropdown-optgroup-marked: $ng-select-dropdown-optgroup-text !default;
// $ng-select-dropdown-option-bg: $ng-select-dropdown-bg !default;
// $ng-select-dropdown-option-text: rgba(0, 0, 0, 0.87) !default;
$ng-select-dropdown-option-disabled: pvar(--greyForegroundColor);
$ng-select-input-text: pvar(--mainForegroundColor);
@import '@ng-select/ng-select/scss/default.theme';
.ng-select {
font-size: $ng-select-value-font-size;
@include rounded-line-height-1-5($ng-select-value-font-size);
&.ng-select-focused {
&:not(.ng-select-opened) > .ng-select-container {
border-color: $ng-select-border !important;
}
}
.ng-input > input {
color: pvar(--inputForegroundColor) !important;
}
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
&:not(.ng-option-marked, .ng-option-selected) {
color: pvar(--mainForegroundColor);
background-color: pvar(--mainBackgroundColor);
}
}
.ng-select-container {
background-color: pvar(--inputBackgroundColor);
}
.ng-arrow-wrapper {
@include padding-right(12px);
}
.ng-arrow {
border-color: #000 transparent transparent !important;
}
&.ng-select-opened .ng-arrow {
border-color: transparent transparent #000 !important;
}
&.ng-select-single .ng-value-container .ng-value {
color: pvar(--inputForegroundColor);
.ng-value-label {
display: flex;
align-items: center;
}
}
&.ng-select-multiple .ng-select-container .ng-value-container {
@include padding-left(12px);
.ng-value {
background-color: pvar(--mainColorLightest);
@include margin-left(12px);
.ng-value-icon {
border: 0 !important;
}
}
}
}

View File

@ -146,19 +146,19 @@ body .p-paginator .p-dropdown {
// Dropdown
body .p-dropdown {
background: #ffffff;
border: 1px solid #a6a6a6;
background: pvar(--mainBackgroundColor);;
border: 1px solid pvar(--inputBorderColor);
transition: border-color 0.2s, box-shadow 0.2s;
border-radius: 6px;
border-radius: 3px;
}
body .p-dropdown:not(.p-disabled):hover {
border-color: #212121;
border-color: pvar(--inputBorderColor);
}
body .p-dropdown:not(.p-disabled).p-focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2em pvar(--mainColorLightest);
border-color: pvar(--mainColor);
border-color: pvar(--inputBorderColor);
}
body .p-dropdown.p-dropdown-clearable .p-dropdown-label {
@include padding-right(0);
@ -168,9 +168,9 @@ body .p-dropdown .p-dropdown-label {
border: 0 none;
}
body .p-dropdown .p-dropdown-label.p-placeholder {
color: #6c757d;
color: pvar(--inputPlaceholderColor);
}
body .p-dropdown .p-dropdown-label:enabled:focus {
.p-dropdown .p-dropdown-label:focus, .p-dropdown .p-dropdown-label:enabled:focus {
outline: 0 none;
box-shadow: none;
}
@ -190,25 +190,24 @@ body .p-dropdown.p-dropdown-clearable .p-dropdown-label {
@include padding-right(4em);
}
body .p-dropdown-panel {
background-color: #ffffff;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16);
border: 0 none;
background-color: pvar(--mainBackgroundColor);
border: 1px solid pvar(--inputBorderColor);
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
body .p-dropdown-panel .p-dropdown-filter-container {
.p-dropdown-panel .p-dropdown-header {
padding: 0.429em 0.857em 0.429em 0.857em;
border-bottom: 1px solid #eaeaea;
color: #333333;
background-color: #ffffff;
border-bottom: 1px solid pvar(--mainColorVeryLight);
color: pvar(--mainForegroundColor);
background-color: pvar(--mainBackgroundColor);
margin: 0;
}
body .p-dropdown-panel .p-dropdown-filter-container .p-dropdown-filter {
.p-dropdown-panel .p-dropdown-header .p-dropdown-filter {
@include padding-right(2em);
width: 100%;
}
body .p-dropdown-panel .p-dropdown-filter-container .p-dropdown-filter-icon {
.p-dropdown-panel .p-dropdown-header .p-dropdown-filter-icon {
top: 50%;
margin-top: -0.5em;
right: 1.357em;
@ -216,29 +215,47 @@ body .p-dropdown-panel .p-dropdown-filter-container .p-dropdown-filter-icon {
}
body .p-dropdown-panel .p-dropdown-items {
padding: 0;
}
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item, body .p-dropdown-panel .p-dropdown-items .p-dropdown-item-group {
margin: 0;
padding: 0.429em 0.857em;
}
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item {
margin: 0;
padding: 8px 16px;
border: 0 none;
color: #333333;
color: pvar(--mainForegroundColor);
background-color: transparent;
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
}
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight,
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item-group.p-highlight {
color: #ffffff;
background-color: pvar(--mainColor);
.p-dropdown-panel .p-dropdown-items .p-dropdown-item:first-child {
margin-top: 0;
}
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item:not(.p-highlight):not(.p-disabled):hover,
body .p-dropdown-panel .p-dropdown-items .p-dropdown-item-group:not(.p-highlight):not(.p-disabled):hover {
color: #333333;
background-color: #eaeaea;
.p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight {
color: pvar(--mainForegroundColor);
background: pvar(--mainColorLightest);
}
body p-dropdown.ng-dirty.ng-invalid > .p-dropdown {
border: 1px solid #a80000;
.p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight.p-focus {
background: pvar(--mainColorLightest);
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-item:not(.p-highlight):not(.p-disabled).p-focus {
color: pvar(--mainForegroundColor);
background: pvar(--mainColorVeryLight);;
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-item:not(.p-highlight):not(.p-disabled):hover {
color: pvar(--mainForegroundColor);
background: pvar(--mainColorVeryLight);;
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-item-group {
margin: 0;
padding: 0.857rem;
color: pvar(--mainForegroundColor);
background: pvar(--mainColorVeryLight);;
font-weight: $font-semibold;
}
.p-dropdown-panel .p-dropdown-items .p-dropdown-empty-message {
padding: 8px 16px;
color: pvar(--mainForegroundColor);
background: transparent;
}
// p-toast
@ -508,6 +525,178 @@ p-chips.p-chips-clearable .p-chips-clear-icon {
right: 0.429rem;
}
// multiselect
.p-multiselect {
background: pvar(--mainBackgroundColor);
border: 1px solid pvar(--inputBorderColor);
transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
border-radius: 3px;
font-size: 15px;
}
.p-multiselect:not(.p-disabled):hover {
border-color: pvar(--inputBorderColor);;
}
.p-multiselect:not(.p-disabled).p-focus {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.25rem var(--mainColorLightest);
}
.p-multiselect .p-multiselect-label {
padding: 4px 15px;
line-height: normal;
transition: background-color 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.p-multiselect .p-multiselect-label.p-placeholder {
color: pvar(--inputPlaceholderColor);
}
.p-multiselect.p-multiselect-chip .p-multiselect-token {
padding: 2px 7px;
margin-right: 0.25rem;
background: pvar(--mainColorLightest);
color: pvar(--mainForegroundColor);
border-radius: 3px;
}
.p-multiselect.p-multiselect-chip .p-multiselect-token .p-multiselect-token-icon {
margin-left: 0.5rem;
}
.p-multiselect .p-multiselect-trigger {
background: transparent;
width: 2.357rem;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.p-multiselect.p-variant-filled {
background: pvar(--mainColorLightest);
}
.p-multiselect.p-variant-filled:not(.p-disabled):hover {
background-color: pvar(--mainColorLightest);
}
.p-multiselect.p-variant-filled:not(.p-disabled).p-focus {
background-color: pvar(--mainColorLightest);
}
.p-inputwrapper-filled .p-multiselect.p-multiselect-chip .p-multiselect-label {
padding: 3px 15px;
}
.p-multiselect-clearable .p-multiselect-label-container {
padding-right: 1.429rem;
}
.p-multiselect-clearable .p-multiselect-clear-icon {
color: pvar(--greyForegroundColor);
right: 2.357rem;
}
.p-multiselect-panel {
background: pvar(--mainBackgroundColor);
color: pvar(--mainForegroundColor);
border: 1px solid pvar(--inputBorderColor);
border-radius: 3px;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, 0.16);
}
.p-multiselect-panel .p-multiselect-header {
padding: 0.429rem 0.857rem;
border-bottom: 1px solid pvar(--inputBorderColor);
color: pvar(--mainForegroundColor);
background: pvar(--mainBackgroundColor);
margin: 0;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-filter-container .p-inputtext {
padding-right: 1.429rem;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-filter-container .p-multiselect-filter-icon {
right: 0.429rem;
color: pvar(--greyForegroundColor);
}
.p-multiselect-panel .p-multiselect-header .p-checkbox {
margin-right: 0.5rem;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-close {
margin-left: 0.5rem;
width: 2rem;
height: 2rem;
color: pvar(--greyForegroundColor);
border: 0 none;
background: transparent;
border-radius: 50%;
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:enabled:hover {
color: pvar(--mainColor);
border-color: transparent;
background: transparent;
}
.p-multiselect-panel .p-multiselect-header .p-multiselect-close:focus-visible {
outline: 0 none;
outline-offset: 0;
box-shadow: 0 0 0 0.2rem pvar(--mainColorVeryLight);
}
.p-multiselect-panel .p-multiselect-items {
padding: 0;
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item {
margin: 0;
padding: 6px 14px;
border: 0 none;
color: pvar(--mainForegroundColor);
background: transparent;
transition: background-color 0.2s, box-shadow 0.2s;
border-radius: 0;
line-height: normal;
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item:first-child {
margin-top: 0;
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item.p-highlight {
color: pvar(--mainBackgroundColor);
background: pvar(--mainColor);
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item.p-highlight.p-focus {
background: pvar(--mainColor);
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item:not(.p-highlight):not(.p-disabled).p-focus {
color: pvar(--mainForegroundColor);
background: pvar(--mainColorVeryLight);
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item:not(.p-highlight):not(.p-disabled):hover {
color: pvar(--mainForegroundColor);
background: pvar(--mainColorVeryLight);
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item .p-checkbox {
margin-right: 0.5rem;
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-item-group {
margin: 0;
padding: 0.857rem;
color: pvar(--mainForegroundColor);
background-color: pvar(--mainColorVeryLight);
font-weight: $font-semibold;
}
.p-multiselect-panel .p-multiselect-items .p-multiselect-empty-message {
padding: 0.429rem 0.857rem;
color: pvar(--mainForegroundColor);
background: transparent;
}
.p-input-filled .p-multiselect {
background-color: pvar(--mainColorVeryLight);
}
.p-input-filled .p-multiselect:not(.p-disabled):hover {
background-color: pvar(--mainColorVeryLight);
}
.p-input-filled .p-multiselect:not(.p-disabled).p-focus {
background-color: pvar(--mainColorVeryLight);
}
p-multiselect.ng-dirty.ng-invalid > .p-multiselect {
border-color: pvar(--red);
}
// input text (used by p-chips)
.p-inputtext {
font-size: 15px;
color: pvar(--mainForegroundColor);
@ -1025,3 +1214,27 @@ p-chips {
flex-basis: 100px;
}
}
.p-multiselect {
width: 100%;
&.p-multiselect-chip .p-multiselect-token {
line-height: normal;
}
}
.p-dropdown-clear-icon,
.p-multiselect-clear-icon {
margin-top: -.5em;
}
.p-dropdown {
width: 100%;
font-size: 15px;
.p-dropdown-label {
padding: 4px 15px;
line-height: normal;
min-height: 29px;
}
}

View File

@ -1,3 +1,3 @@
import { JobState } from '@peertube/peertube-models'
export type JobStateClient = JobState
export type JobStateClient = JobState | 'all'

View File

@ -1,13 +1,14 @@
export interface SelectOptionsItem {
id: string | number
label: string
description?: string
group?: string
groupLabel?: string
imageUrl?: string
classes?: string[]
}
export interface SelectChannelItem extends SelectOptionsItem {
id: number
support: string
id: number // Force number
avatarPath?: string
support?: string
}

View File

@ -2034,13 +2034,6 @@
dependencies:
tslib "^2.3.0"
"@ng-select/ng-select@^13.8.1":
version "13.8.1"
resolved "https://registry.yarnpkg.com/@ng-select/ng-select/-/ng-select-13.8.1.tgz#4884fa51aeccd534e3dd646385405248c867da68"
integrity sha512-zN+uYkTOZliRxEm9zS7M08g21YXaNsBLp9/zJgjC5TzNrrfB6vxgOzbDjYzNUqSbZsbod70HoPgzmi81pJhMyg==
dependencies:
tslib "^2.3.1"
"@ngtools/webpack@18.2.0":
version "18.2.0"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-18.2.0.tgz#7977e2686020e00ace813a81445cb487dd5ee56b"
@ -10890,7 +10883,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.6.3, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2:
tslib@2.6.3, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==