diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-07-23 21:30:04 +0200 |
---|---|---|
committer | Rigel Kent <par@rigelk.eu> | 2020-07-29 18:15:53 +0200 |
commit | 4f5d045960b042eb27e10bac1bdaf1c074c9fa2a (patch) | |
tree | 09e1e8cce0a2e64146ede51941cfa2f1bdcf3c2f /client/src | |
parent | bc99dfe54e093e69ba8fd06d36b36fbbda3f45de (diff) | |
download | PeerTube-4f5d045960b042eb27e10bac1bdaf1c074c9fa2a.tar.gz PeerTube-4f5d045960b042eb27e10bac1bdaf1c074c9fa2a.tar.zst PeerTube-4f5d045960b042eb27e10bac1bdaf1c074c9fa2a.zip |
harmonize search for libraries
Diffstat (limited to 'client/src')
26 files changed, 225 insertions, 87 deletions
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 571c780d6..e8a084259 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -112,8 +112,10 @@ | |||
112 | </a> | 112 | </a> |
113 | </td> | 113 | </td> |
114 | 114 | ||
115 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email"> | 115 | <td *ngIf="getColumn('email')" [title]="user.email"> |
116 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | 116 | <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> |
117 | <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a> | ||
118 | </ng-container> | ||
117 | </td> | 119 | </td> |
118 | 120 | ||
119 | <ng-template #emailWithVerificationStatus> | 121 | <ng-template #emailWithVerificationStatus> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index b2978212e..699b2a6da 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -7,6 +7,13 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
7 | import { ServerConfig, User, UserRole } from '@shared/models' | 7 | import { ServerConfig, User, UserRole } from '@shared/models' |
8 | import { Params, Router, ActivatedRoute } from '@angular/router' | 8 | import { Params, Router, ActivatedRoute } from '@angular/router' |
9 | 9 | ||
10 | type UserForList = User & { | ||
11 | rawVideoQuota: number | ||
12 | rawVideoQuotaUsed: number | ||
13 | rawVideoQuotaDaily: number | ||
14 | rawVideoQuotaUsedDaily: number | ||
15 | } | ||
16 | |||
10 | @Component({ | 17 | @Component({ |
11 | selector: 'my-user-list', | 18 | selector: 'my-user-list', |
12 | templateUrl: './user-list.component.html', | 19 | templateUrl: './user-list.component.html', |
@@ -24,8 +31,8 @@ export class UserListComponent extends RestTable implements OnInit { | |||
24 | selectedUsers: User[] = [] | 31 | selectedUsers: User[] = [] |
25 | bulkUserActions: DropdownAction<User[]>[][] = [] | 32 | bulkUserActions: DropdownAction<User[]>[][] = [] |
26 | columns: { key: string, label: string }[] | 33 | columns: { key: string, label: string }[] |
27 | _selectedColumns: { key: string, label: string }[] | ||
28 | 34 | ||
35 | private _selectedColumns: { key: string, label: string }[] | ||
29 | private serverConfig: ServerConfig | 36 | private serverConfig: ServerConfig |
30 | 37 | ||
31 | constructor ( | 38 | constructor ( |
@@ -111,7 +118,7 @@ export class UserListComponent extends RestTable implements OnInit { | |||
111 | { key: 'role', label: 'Role' }, | 118 | { key: 'role', label: 'Role' }, |
112 | { key: 'createdAt', label: 'Created' } | 119 | { key: 'createdAt', label: 'Created' } |
113 | ] | 120 | ] |
114 | this.selectedColumns = [...this.columns] | 121 | this.selectedColumns = [ ...this.columns ] // make a full copy of the array |
115 | this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) | 122 | this.columns.push({ key: 'quotaDaily', label: 'Daily quota' }) |
116 | this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) | 123 | this.columns.push({ key: 'pluginAuth', label: 'Auth plugin' }) |
117 | this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) | 124 | this.columns.push({ key: 'lastLoginDate', label: 'Last login' }) |
@@ -133,14 +140,14 @@ export class UserListComponent extends RestTable implements OnInit { | |||
133 | } | 140 | } |
134 | 141 | ||
135 | getColumn (key: string) { | 142 | getColumn (key: string) { |
136 | return this.selectedColumns.find((col: any) => col.key === key) | 143 | return this.selectedColumns.find((col: { key: string }) => col.key === key) |
137 | } | 144 | } |
138 | 145 | ||
139 | getUserVideoQuotaPercentage (user: User & { rawVideoQuota: number, rawVideoQuotaUsed: number}) { | 146 | getUserVideoQuotaPercentage (user: UserForList) { |
140 | return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota | 147 | return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota |
141 | } | 148 | } |
142 | 149 | ||
143 | getUserVideoQuotaDailyPercentage (user: User & { rawVideoQuotaDaily: number, rawVideoQuotaUsedDaily: number}) { | 150 | getUserVideoQuotaDailyPercentage (user: UserForList) { |
144 | return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily | 151 | return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily |
145 | } | 152 | } |
146 | 153 | ||
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html index e8d44a45e..c20215cf9 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.html | |||
@@ -1,14 +1,21 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My channels</ng-container> | 3 | <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My channels</ng-container> |
5 | <span class="badge badge-secondary">{{ totalItems }}</span> | ||
6 | </span> | ||
7 | |||
8 | <div class="has-feedback has-clear"> | ||
9 | <input type="text" placeholder="Search your channels" i18n-placeholder [(ngModel)]="channelsSearch" (ngModelChange)="onChannelsSearchChanged()" /> | ||
10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
5 | 13 | ||
6 | <div class="video-channels-header"> | ||
7 | <a class="create-button" routerLink="create"> | 14 | <a class="create-button" routerLink="create"> |
8 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | 15 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> |
9 | <ng-container i18n>Create video channel</ng-container> | 16 | <ng-container i18n>Create video channel</ng-container> |
10 | </a> | 17 | </a> |
11 | </div> | 18 | </h1> |
12 | 19 | ||
13 | <div class="video-channels"> | 20 | <div class="video-channels"> |
14 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> | 21 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> |
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss index 76fb2cde0..4ecb4f408 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include create-button; | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | input[type=text] { | ||
9 | @include peertube-input-text(300px); | ||
10 | } | ||
11 | |||
8 | ::ng-deep .action-button { | 12 | ::ng-deep .action-button { |
9 | &.action-button-edit { | 13 | &.action-button-edit { |
10 | margin-right: 10px; | 14 | margin-right: 10px; |
@@ -55,11 +59,6 @@ | |||
55 | } | 59 | } |
56 | } | 60 | } |
57 | 61 | ||
58 | .video-channels-header { | ||
59 | text-align: right; | ||
60 | margin: 20px 0 50px; | ||
61 | } | ||
62 | |||
63 | ::ng-deep .chartjs-render-monitor { | 62 | ::ng-deep .chartjs-render-monitor { |
64 | position: relative; | 63 | position: relative; |
65 | top: 1px; | 64 | top: 1px; |
diff --git a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts index 70510d7c9..da8c7298f 100644 --- a/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts +++ b/client/src/app/+my-account/+my-account-video-channels/my-account-video-channels.component.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { ChartData } from 'chart.js' | 1 | import { ChartData } from 'chart.js' |
2 | import { max, maxBy, min, minBy } from 'lodash-es' | 2 | import { max, maxBy, min, minBy } from 'lodash-es' |
3 | import { flatMap } from 'rxjs/operators' | 3 | import { flatMap, debounceTime } from 'rxjs/operators' |
4 | import { Component, OnInit } from '@angular/core' | 4 | import { Component, OnInit } from '@angular/core' |
5 | import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' | 5 | import { AuthService, ConfirmService, Notifier, ScreenService, User } from '@app/core' |
6 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 6 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { Subject } from 'rxjs' | ||
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-account-video-channels', | 11 | selector: 'my-account-video-channels', |
@@ -12,11 +13,16 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
12 | styleUrls: [ './my-account-video-channels.component.scss' ] | 13 | styleUrls: [ './my-account-video-channels.component.scss' ] |
13 | }) | 14 | }) |
14 | export class MyAccountVideoChannelsComponent implements OnInit { | 15 | export class MyAccountVideoChannelsComponent implements OnInit { |
16 | totalItems: number | ||
17 | |||
15 | videoChannels: VideoChannel[] = [] | 18 | videoChannels: VideoChannel[] = [] |
16 | videoChannelsChartData: ChartData[] | 19 | videoChannelsChartData: ChartData[] |
17 | videoChannelsMinimumDailyViews = 0 | 20 | videoChannelsMinimumDailyViews = 0 |
18 | videoChannelsMaximumDailyViews: number | 21 | videoChannelsMaximumDailyViews: number |
19 | 22 | ||
23 | channelsSearch: string | ||
24 | channelsSearchChanged = new Subject<string>() | ||
25 | |||
20 | private user: User | 26 | private user: User |
21 | 27 | ||
22 | constructor ( | 28 | constructor ( |
@@ -32,6 +38,12 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
32 | this.user = this.authService.getUser() | 38 | this.user = this.authService.getUser() |
33 | 39 | ||
34 | this.loadVideoChannels() | 40 | this.loadVideoChannels() |
41 | |||
42 | this.channelsSearchChanged | ||
43 | .pipe(debounceTime(500)) | ||
44 | .subscribe(() => { | ||
45 | this.loadVideoChannels() | ||
46 | }) | ||
35 | } | 47 | } |
36 | 48 | ||
37 | get isInSmallView () { | 49 | get isInSmallView () { |
@@ -87,6 +99,15 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
87 | } | 99 | } |
88 | } | 100 | } |
89 | 101 | ||
102 | resetSearch() { | ||
103 | this.channelsSearch = '' | ||
104 | this.onChannelsSearchChanged() | ||
105 | } | ||
106 | |||
107 | onChannelsSearchChanged () { | ||
108 | this.channelsSearchChanged.next() | ||
109 | } | ||
110 | |||
90 | async deleteVideoChannel (videoChannel: VideoChannel) { | 111 | async deleteVideoChannel (videoChannel: VideoChannel) { |
91 | const res = await this.confirmService.confirmWithInput( | 112 | const res = await this.confirmService.confirmWithInput( |
92 | this.i18n( | 113 | this.i18n( |
@@ -118,9 +139,10 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
118 | 139 | ||
119 | private loadVideoChannels () { | 140 | private loadVideoChannels () { |
120 | this.authService.userInformationLoaded | 141 | this.authService.userInformationLoaded |
121 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) | 142 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true, this.channelsSearch))) |
122 | .subscribe(res => { | 143 | .subscribe(res => { |
123 | this.videoChannels = res.data | 144 | this.videoChannels = res.data |
145 | this.totalItems = res.total | ||
124 | 146 | ||
125 | // chart data | 147 | // chart data |
126 | this.videoChannelsChartData = this.videoChannels.map(v => ({ | 148 | this.videoChannelsChartData = this.videoChannels.map(v => ({ |
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html index 9b5f2dd2f..8de152b5e 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -6,10 +6,10 @@ | |||
6 | </a> | 6 | </a> |
7 | 7 | ||
8 | <div class="peertube-select-container peertube-select-button ml-2"> | 8 | <div class="peertube-select-container peertube-select-button ml-2"> |
9 | <select [(ngModel)]="notificationSortType" class="form-control"> | 9 | <select [(ngModel)]="notificationSortType" (ngModelChange)="onChangeSortColumn()" class="form-control"> |
10 | <option value="undefined" disabled>Sort by</option> | 10 | <option value="undefined" disabled>Sort by</option> |
11 | <option value="created" i18n>Newest first</option> | 11 | <option value="createdAt" i18n>Newest first</option> |
12 | <option value="unread-created" i18n>Unread first</option> | 12 | <option value="read" [disabled]="!hasUnreadNotifications()" i18n>Unread first</option> |
13 | </select> | 13 | </select> |
14 | </div> | 14 | </div> |
15 | 15 | ||
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts index 8a51319fe..0ec67d401 100644 --- a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { Component, ViewChild } from '@angular/core' | 1 | import { Component, ViewChild } from '@angular/core' |
2 | import { UserNotificationsComponent } from '@app/shared/shared-main' | 2 | import { UserNotificationsComponent } from '@app/shared/shared-main' |
3 | 3 | ||
4 | type NotificationSortType = 'createdAt' | 'read' | ||
5 | |||
4 | @Component({ | 6 | @Component({ |
5 | templateUrl: './my-account-notifications.component.html', | 7 | templateUrl: './my-account-notifications.component.html', |
6 | styleUrls: [ './my-account-notifications.component.scss' ] | 8 | styleUrls: [ './my-account-notifications.component.scss' ] |
@@ -8,7 +10,17 @@ import { UserNotificationsComponent } from '@app/shared/shared-main' | |||
8 | export class MyAccountNotificationsComponent { | 10 | export class MyAccountNotificationsComponent { |
9 | @ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent | 11 | @ViewChild('userNotification', { static: true }) userNotification: UserNotificationsComponent |
10 | 12 | ||
11 | notificationSortType = 'created' | 13 | _notificationSortType: NotificationSortType = 'createdAt' |
14 | |||
15 | get notificationSortType () { | ||
16 | return !this.hasUnreadNotifications() | ||
17 | ? 'createdAt' | ||
18 | : this._notificationSortType | ||
19 | } | ||
20 | |||
21 | set notificationSortType (type: NotificationSortType) { | ||
22 | this._notificationSortType = type | ||
23 | } | ||
12 | 24 | ||
13 | markAllAsRead () { | 25 | markAllAsRead () { |
14 | this.userNotification.markAllAsRead() | 26 | this.userNotification.markAllAsRead() |
@@ -17,4 +29,8 @@ export class MyAccountNotificationsComponent { | |||
17 | hasUnreadNotifications () { | 29 | hasUnreadNotifications () { |
18 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 | 30 | return this.userNotification.notifications.filter(n => n.read === false).length !== 0 |
19 | } | 31 | } |
32 | |||
33 | onChangeSortColumn () { | ||
34 | this.userNotification.changeSortColumn(this.notificationSortType) | ||
35 | } | ||
20 | } | 36 | } |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html index be5d41f3b..4475178c7 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html | |||
@@ -62,7 +62,11 @@ | |||
62 | </td> | 62 | </td> |
63 | 63 | ||
64 | <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> | 64 | <td>{{ videoChangeOwnership.createdAt | date: 'short' }}</td> |
65 | <td i18n>{{ videoChangeOwnership.status }}</td> | 65 | |
66 | <td> | ||
67 | <span class="badge" [ngClass]="getStatusClass(videoChangeOwnership.status)">{{ videoChangeOwnership.status }}</span> | ||
68 | </td> | ||
69 | |||
66 | <td class="action-cell"> | 70 | <td class="action-cell"> |
67 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | 71 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> |
68 | <my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button> | 72 | <my-button i18n-label label="Accept" icon="tick" (click)="openAcceptModal(videoChangeOwnership)"></my-button> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss index c04e26374..7cac9c9f3 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include chip; | 5 | @include chip; |
6 | } | 6 | } |
7 | 7 | ||
8 | .badge { | ||
9 | @include table-badge; | ||
10 | } | ||
11 | |||
8 | .video-table-video { | 12 | .video-table-video { |
9 | display: inline-flex; | 13 | display: inline-flex; |
10 | 14 | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts index 98360dfb3..7473470aa 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts | |||
@@ -2,7 +2,7 @@ import { SortMeta } from 'primeng/api' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' | 4 | import { VideoOwnershipService, Actor, Video, Account } from '@app/shared/shared-main' |
5 | import { VideoChangeOwnership } from '@shared/models' | 5 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '@shared/models' |
6 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' | 6 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' |
7 | import { getAbsoluteAPIUrl } from '@app/helpers' | 7 | import { getAbsoluteAPIUrl } from '@app/helpers' |
8 | 8 | ||
@@ -34,6 +34,17 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
34 | return 'MyAccountOwnershipComponent' | 34 | return 'MyAccountOwnershipComponent' |
35 | } | 35 | } |
36 | 36 | ||
37 | getStatusClass (status: VideoChangeOwnershipStatus) { | ||
38 | switch (status) { | ||
39 | case VideoChangeOwnershipStatus.ACCEPTED: | ||
40 | return 'badge-green' | ||
41 | case VideoChangeOwnershipStatus.REFUSED: | ||
42 | return 'badge-red' | ||
43 | default: | ||
44 | return 'badge-yellow' | ||
45 | } | ||
46 | } | ||
47 | |||
37 | switchToDefaultAvatar ($event: Event) { | 48 | switchToDefaultAvatar ($event: Event) { |
38 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() | 49 | ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() |
39 | } | 50 | } |
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html index 3b4c3022e..6cec7c0d5 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html | |||
@@ -1,6 +1,15 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My subscriptions</ng-container> | 3 | <my-global-icon iconName="subscriptions" aria-hidden="true"></my-global-icon> |
4 | <ng-container i18n>My subscriptions</ng-container> | ||
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
7 | |||
8 | <div class="has-feedback has-clear"> | ||
9 | <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" (ngModelChange)="onSubscriptionsSearchChanged()" /> | ||
10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
4 | </h1> | 13 | </h1> |
5 | 14 | ||
6 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> | 15 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">You don't have any subscriptions yet.</div> |
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss index dd990c42b..884959070 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | input[type=text] { | ||
5 | @include peertube-input-text(300px); | ||
6 | } | ||
7 | |||
4 | .video-channel { | 8 | .video-channel { |
5 | @include row-blocks; | 9 | @include row-blocks; |
6 | 10 | ||
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts index 390293a28..994fe5142 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts | |||
@@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core' | |||
3 | import { ComponentPagination, Notifier } from '@app/core' | 3 | import { ComponentPagination, Notifier } from '@app/core' |
4 | import { VideoChannel } from '@app/shared/shared-main' | 4 | import { VideoChannel } from '@app/shared/shared-main' |
5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' | 5 | import { UserSubscriptionService } from '@app/shared/shared-user-subscription' |
6 | import { debounceTime } from 'rxjs/operators' | ||
6 | 7 | ||
7 | @Component({ | 8 | @Component({ |
8 | selector: 'my-account-subscriptions', | 9 | selector: 'my-account-subscriptions', |
@@ -20,6 +21,9 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
20 | 21 | ||
21 | onDataSubject = new Subject<any[]>() | 22 | onDataSubject = new Subject<any[]>() |
22 | 23 | ||
24 | subscriptionsSearch: string | ||
25 | subscriptionsSearchChanged = new Subject<string>() | ||
26 | |||
23 | constructor ( | 27 | constructor ( |
24 | private userSubscriptionService: UserSubscriptionService, | 28 | private userSubscriptionService: UserSubscriptionService, |
25 | private notifier: Notifier | 29 | private notifier: Notifier |
@@ -27,20 +31,22 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
27 | 31 | ||
28 | ngOnInit () { | 32 | ngOnInit () { |
29 | this.loadSubscriptions() | 33 | this.loadSubscriptions() |
30 | } | ||
31 | 34 | ||
32 | loadSubscriptions () { | 35 | this.subscriptionsSearchChanged |
33 | this.userSubscriptionService.listSubscriptions(this.pagination) | 36 | .pipe(debounceTime(500)) |
34 | .subscribe( | 37 | .subscribe(() => { |
35 | res => { | 38 | this.pagination.currentPage = 1 |
36 | this.videoChannels = this.videoChannels.concat(res.data) | 39 | this.loadSubscriptions(false) |
37 | this.pagination.totalItems = res.total | 40 | }) |
41 | } | ||
38 | 42 | ||
39 | this.onDataSubject.next(res.data) | 43 | resetSearch () { |
40 | }, | 44 | this.subscriptionsSearch = '' |
45 | this.onSubscriptionsSearchChanged() | ||
46 | } | ||
41 | 47 | ||
42 | error => this.notifier.error(error.message) | 48 | onSubscriptionsSearchChanged () { |
43 | ) | 49 | this.subscriptionsSearchChanged.next() |
44 | } | 50 | } |
45 | 51 | ||
46 | onNearOfBottom () { | 52 | onNearOfBottom () { |
@@ -51,4 +57,19 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
51 | this.loadSubscriptions() | 57 | this.loadSubscriptions() |
52 | } | 58 | } |
53 | 59 | ||
60 | private loadSubscriptions (more = true) { | ||
61 | this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.subscriptionsSearch }) | ||
62 | .subscribe( | ||
63 | res => { | ||
64 | this.videoChannels = more | ||
65 | ? this.videoChannels.concat(res.data) | ||
66 | : res.data | ||
67 | this.pagination.totalItems = res.total | ||
68 | |||
69 | this.onDataSubject.next(res.data) | ||
70 | }, | ||
71 | |||
72 | error => this.notifier.error(error.message) | ||
73 | ) | ||
74 | } | ||
54 | } | 75 | } |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html index 98a2039cc..854126443 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html | |||
@@ -45,7 +45,12 @@ | |||
45 | <ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container> | 45 | <ng-container *ngIf="isVideoImportFailed(videoImport)"></ng-container> |
46 | </td> | 46 | </td> |
47 | 47 | ||
48 | <td>{{ videoImport.state.label }}</td> | 48 | <td> |
49 | <span class="badge" [ngClass]="getVideoImportStateClass(videoImport.state)"> | ||
50 | {{ videoImport.state.label }} | ||
51 | </span> | ||
52 | </td> | ||
53 | |||
49 | <td>{{ videoImport.createdAt | date: 'short' }}</td> | 54 | <td>{{ videoImport.createdAt | date: 'short' }}</td> |
50 | 55 | ||
51 | <td class="action-cell"> | 56 | <td class="action-cell"> |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss index bdd2f8270..a93c28028 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss | |||
@@ -7,4 +7,8 @@ pre { | |||
7 | 7 | ||
8 | .video-import-error { | 8 | .video-import-error { |
9 | color: red; | 9 | color: red; |
10 | } \ No newline at end of file | 10 | } |
11 | |||
12 | .badge { | ||
13 | @include table-badge; | ||
14 | } | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts index 42ddb0ee2..9dd5ef142 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts | |||
@@ -30,6 +30,19 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit | |||
30 | return 'MyAccountVideoImportsComponent' | 30 | return 'MyAccountVideoImportsComponent' |
31 | } | 31 | } |
32 | 32 | ||
33 | getVideoImportStateClass (state: VideoImportState) { | ||
34 | switch (state) { | ||
35 | case VideoImportState.FAILED: | ||
36 | return 'badge-red' | ||
37 | case VideoImportState.REJECTED: | ||
38 | return 'badge-banned' | ||
39 | case VideoImportState.PENDING: | ||
40 | return 'badge-yellow' | ||
41 | default: | ||
42 | return 'badge-green' | ||
43 | } | ||
44 | } | ||
45 | |||
33 | isVideoImportSuccess (videoImport: VideoImport) { | 46 | isVideoImportSuccess (videoImport: VideoImport) { |
34 | return videoImport.state.id === VideoImportState.SUCCESS | 47 | return videoImport.state.id === VideoImportState.SUCCESS |
35 | } | 48 | } |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html index 8d69c3a5a..d8e3fb2fa 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.html | |||
@@ -1,17 +1,20 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> | 3 | <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span> |
5 | 5 | </span> | |
6 | 6 | ||
7 | <div class="video-playlists-header"> | 7 | <div class="has-feedback has-clear"> |
8 | <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> | 8 | <input type="text" placeholder="Search your playlists" i18n-placeholder [(ngModel)]="videoPlaylistsSearch" (ngModelChange)="onVideoPlaylistSearchChanged()" /> |
9 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> | ||
10 | <span class="sr-only" i18n>Clear filters</span> | ||
11 | </div> | ||
9 | 12 | ||
10 | <a class="create-button" routerLink="create"> | 13 | <a class="create-button" routerLink="create"> |
11 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> | 14 | <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> |
12 | <ng-container i18n>Create playlist</ng-container> | 15 | <ng-container i18n>Create playlist</ng-container> |
13 | </a> | 16 | </a> |
14 | </div> | 17 | </h1> |
15 | 18 | ||
16 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> | 19 | <div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> |
17 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> | 20 | <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss index 4381d74b0..ade28c70b 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.scss | |||
@@ -5,6 +5,10 @@ | |||
5 | @include create-button; | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | input[type=text] { | ||
9 | @include peertube-input-text(300px); | ||
10 | } | ||
11 | |||
8 | ::ng-deep .action-button { | 12 | ::ng-deep .action-button { |
9 | &.action-button-delete { | 13 | &.action-button-delete { |
10 | margin-right: 10px; | 14 | margin-right: 10px; |
@@ -33,16 +37,6 @@ | |||
33 | } | 37 | } |
34 | } | 38 | } |
35 | 39 | ||
36 | .video-playlists-header { | ||
37 | display: flex; | ||
38 | justify-content: space-between; | ||
39 | margin: 20px 0 50px; | ||
40 | |||
41 | input[type=text] { | ||
42 | @include peertube-input-text(300px); | ||
43 | } | ||
44 | } | ||
45 | |||
46 | @media screen and (max-width: $small-view) { | 40 | @media screen and (max-width: $small-view) { |
47 | .video-playlists-header { | 41 | .video-playlists-header { |
48 | text-align: center; | 42 | text-align: center; |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts index ea3bcde4f..668a23d8f 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlists.component.ts | |||
@@ -84,6 +84,11 @@ export class MyAccountVideoPlaylistsComponent implements OnInit { | |||
84 | this.loadVideoPlaylists() | 84 | this.loadVideoPlaylists() |
85 | } | 85 | } |
86 | 86 | ||
87 | resetSearch () { | ||
88 | this.videoPlaylistsSearch = '' | ||
89 | this.onVideoPlaylistSearchChanged() | ||
90 | } | ||
91 | |||
87 | onVideoPlaylistSearchChanged () { | 92 | onVideoPlaylistSearchChanged () { |
88 | this.videoPlaylistSearchChanged.next() | 93 | this.videoPlaylistSearchChanged.next() |
89 | } | 94 | } |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index 6d098b507..faeb3b56c 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -1,11 +1,16 @@ | |||
1 | <h1> | 1 | <h1 class="d-flex justify-content-between"> |
2 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> | 2 | <span> |
3 | <ng-container i18n>My videos</ng-container><span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | 3 | <my-global-icon iconName="videos" aria-hidden="true"></my-global-icon> |
4 | </h1> | 4 | <ng-container i18n>My videos</ng-container> |
5 | <span class="badge badge-secondary"> {{ pagination.totalItems }}</span> | ||
6 | </span> | ||
5 | 7 | ||
6 | <div class="videos-header"> | 8 | <div class="has-feedback has-clear"> |
7 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" /> | 9 | <input type="text" placeholder="Search your videos" i18n-placeholder [(ngModel)]="videosSearch" (ngModelChange)="onVideosSearchChanged()" /> |
8 | </div> | 10 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> |
11 | <span class="sr-only" i18n>Clear filters</span> | ||
12 | </div> | ||
13 | </h1> | ||
9 | 14 | ||
10 | <my-videos-selection | 15 | <my-videos-selection |
11 | [pagination]="pagination" | 16 | [pagination]="pagination" |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 9225fc5fd..0930b1959 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss | |||
@@ -1,14 +1,8 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .videos-header { | 4 | input[type=text] { |
5 | display: flex; | 5 | @include peertube-input-text(300px); |
6 | justify-content: space-between; | ||
7 | margin: 20px 0 50px; | ||
8 | |||
9 | input[type=text] { | ||
10 | @include peertube-input-text(300px); | ||
11 | } | ||
12 | } | 6 | } |
13 | 7 | ||
14 | .action-button-delete-selection { | 8 | .action-button-delete-selection { |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 3cfe8fb38..2274c6a7b 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts | |||
@@ -59,13 +59,17 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { | |||
59 | 59 | ||
60 | ngOnInit () { | 60 | ngOnInit () { |
61 | this.videosSearchChanged | 61 | this.videosSearchChanged |
62 | .pipe( | 62 | .pipe(debounceTime(500)) |
63 | debounceTime(500)) | ||
64 | .subscribe(() => { | 63 | .subscribe(() => { |
65 | this.videosSelection.reloadVideos() | 64 | this.videosSelection.reloadVideos() |
66 | }) | 65 | }) |
67 | } | 66 | } |
68 | 67 | ||
68 | resetSearch () { | ||
69 | this.videosSearch = '' | ||
70 | this.onVideosSearchChanged() | ||
71 | } | ||
72 | |||
69 | onVideosSearchChanged () { | 73 | onVideosSearchChanged () { |
70 | this.videosSearchChanged.next() | 74 | this.videosSearchChanged.next() |
71 | } | 75 | } |
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index 5f9300bec..c98b3844c 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts | |||
@@ -381,14 +381,14 @@ export class UserService { | |||
381 | 381 | ||
382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) | 382 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) |
383 | 383 | ||
384 | let videoQuotaDaily | 384 | let videoQuotaDaily: string |
385 | let videoQuotaUsedDaily | 385 | let videoQuotaUsedDaily: string |
386 | if (user.videoQuotaDaily === -1) { | 386 | if (user.videoQuotaDaily === -1) { |
387 | videoQuotaDaily = '∞' | 387 | videoQuotaDaily = '∞' |
388 | videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) | 388 | videoQuotaUsedDaily = this.bytesPipe.transform(0, 0) + '' |
389 | } else { | 389 | } else { |
390 | videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) | 390 | videoQuotaDaily = this.bytesPipe.transform(user.videoQuotaDaily, 0) + '' |
391 | videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) | 391 | videoQuotaUsedDaily = this.bytesPipe.transform(user.videoQuotaUsedDaily || 0, 0) + '' |
392 | } | 392 | } |
393 | 393 | ||
394 | const roleLabels: { [ id in UserRole ]: string } = { | 394 | const roleLabels: { [ id in UserRole ]: string } = { |
diff --git a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts index 732ed6bcb..eb1fdf91c 100644 --- a/client/src/app/shared/shared-user-subscription/user-subscription.service.ts +++ b/client/src/app/shared/shared-user-subscription/user-subscription.service.ts | |||
@@ -105,13 +105,18 @@ export class UserSubscriptionService { | |||
105 | ) | 105 | ) |
106 | } | 106 | } |
107 | 107 | ||
108 | listSubscriptions (componentPagination: ComponentPaginationLight): Observable<ResultList<VideoChannel>> { | 108 | listSubscriptions (parameters: { |
109 | pagination: ComponentPaginationLight | ||
110 | search: string | ||
111 | }): Observable<ResultList<VideoChannel>> { | ||
112 | const { pagination, search } = parameters | ||
109 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL | 113 | const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL |
110 | 114 | ||
111 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | 115 | const restPagination = this.restService.componentPaginationToRestPagination(pagination) |
112 | 116 | ||
113 | let params = new HttpParams() | 117 | let params = new HttpParams() |
114 | params = this.restService.addRestGetParams(params, pagination) | 118 | params = this.restService.addRestGetParams(params, restPagination) |
119 | if (search) params = params.append('search', search) | ||
115 | 120 | ||
116 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) | 121 | return this.authHttp.get<ResultList<VideoChannelServer>>(url, { params }) |
117 | .pipe( | 122 | .pipe( |
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss index 897182e53..8b7eab366 100644 --- a/client/src/sass/bootstrap.scss +++ b/client/src/sass/bootstrap.scss | |||
@@ -310,6 +310,7 @@ ngb-tooltip-window { | |||
310 | position: absolute; | 310 | position: absolute; |
311 | right: .5rem; | 311 | right: .5rem; |
312 | height: 95%; | 312 | height: 95%; |
313 | font-size: 14px; | ||
313 | 314 | ||
314 | &:hover { | 315 | &:hover { |
315 | color: rgba(0, 0, 0, 0.7); | 316 | color: rgba(0, 0, 0, 0.7); |
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 75fe2ab11..0fb54f121 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -690,12 +690,11 @@ | |||
690 | overflow: hidden; | 690 | overflow: hidden; |
691 | font-size: 0.75rem; | 691 | font-size: 0.75rem; |
692 | border-radius: 0.25rem; | 692 | border-radius: 0.25rem; |
693 | isolation: isolate; | ||
694 | position: relative; | 693 | position: relative; |
695 | 694 | ||
696 | span { | 695 | span { |
697 | position: absolute; | 696 | position: absolute; |
698 | color: rgb(92, 92, 92); | 697 | color: $grey-foreground-color; |
699 | top: -1px; | 698 | top: -1px; |
700 | 699 | ||
701 | &:nth-of-type(1) { | 700 | &:nth-of-type(1) { |