Add action dropdown descriptions

This commit is contained in:
Rigel Kent 2020-01-15 19:25:51 +01:00 committed by Chocobozzz
parent c78d3db71b
commit 9b82d49da8
15 changed files with 88 additions and 52 deletions

View File

@ -23,7 +23,7 @@
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
<my-user-moderation-dropdown <my-user-moderation-dropdown
buttonSize="small" [account]="account" [user]="user" placement="bottom-right auto" buttonSize="small" [account]="account" [user]="user" placement="bottom-left auto"
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
></my-user-moderation-dropdown> ></my-user-moderation-dropdown>
</div> </div>

View File

@ -118,7 +118,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help> <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
<div class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div> <div i18n class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
<my-markdown-textarea <my-markdown-textarea
name="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true" name="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true"
@ -131,7 +131,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceAdministrator">Who is behind the instance?</label> <label i18n for="instanceAdministrator">Who is behind the instance?</label>
<div class="label-small-info">A single person? A non-profit? A company?</div> <div i18n class="label-small-info">A single person? A non-profit? A company?</div>
<my-markdown-textarea <my-markdown-textarea
name="instanceAdministrator" formControlName="administrator" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true" name="instanceAdministrator" formControlName="administrator" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
@ -143,7 +143,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceCreationReason">Why did you create this instance?</label> <label i18n for="instanceCreationReason">Why did you create this instance?</label>
<div class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div> <div i18n class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
<textarea <textarea
id="instanceCreationReason" formControlName="creationReason" class="small" id="instanceCreationReason" formControlName="creationReason" class="small"
@ -154,7 +154,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label> <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label>
<div class="label-small-info">It's important to know for users who want to register on your instance</div> <div i18n class="label-small-info">It's important to know for users who want to register on your instance</div>
<textarea <textarea
id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" class="small" id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" class="small"
@ -165,7 +165,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label> <label i18n for="instanceBusinessModel">How will you finance the PeerTube server?</label>
<div class="label-small-info">With your own funds? With users donations? Advertising?</div> <div i18n class="label-small-info">With your own funds? With users donations? Advertising?</div>
<textarea <textarea
id="instanceBusinessModel" formControlName="businessModel" class="small" id="instanceBusinessModel" formControlName="businessModel" class="small"
@ -178,7 +178,7 @@
<div class="form-group"> <div class="form-group">
<label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label> <label i18n for="instanceHardwareInformation">What server/hardware does the instance run on?</label>
<div class="label-small-info">2vCore 2GB RAM/or directly the link to the server you rent etc</div> <div i18n class="label-small-info">2vCore 2GB RAM/or directly the link to the server you rent etc</div>
<my-markdown-textarea <my-markdown-textarea
name="instanceHardwareInformation" formControlName="hardwareInformation" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true" name="instanceHardwareInformation" formControlName="hardwareInformation" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"

View File

@ -66,7 +66,7 @@
</a> </a>
</td> </td>
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td> <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus" [title]="user.email">{{ user.email }}</td>
<ng-template #emailWithVerificationStatus> <ng-template #emailWithVerificationStatus>
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
@ -81,7 +81,7 @@
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
<td>{{ user.roleLabel }}</td> <td>{{ user.roleLabel }}</td>
<td>{{ user.createdAt }}</td> <td [title]="user.createdAt">{{ user.createdAt }}</td>
<td class="action-cell"> <td class="action-cell">
<my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
</my-user-moderation-dropdown> </my-user-moderation-dropdown>

View File

@ -23,7 +23,7 @@ export class UserListComponent extends RestTable implements OnInit {
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
selectedUsers: User[] = [] selectedUsers: User[] = []
bulkUserActions: DropdownAction<User[]>[] = [] bulkUserActions: DropdownAction<User[]>[][] = []
private serverConfig: ServerConfig private serverConfig: ServerConfig
@ -54,13 +54,16 @@ export class UserListComponent extends RestTable implements OnInit {
this.initialize() this.initialize()
this.bulkUserActions = [ this.bulkUserActions = [
[
{ {
label: this.i18n('Delete'), label: this.i18n('Delete'),
description: this.i18n('Videos will be deleted, comments will be tombstoned.'),
handler: users => this.removeUsers(users), handler: users => this.removeUsers(users),
isDisplayed: users => users.every(u => this.authUser.canManage(u)) isDisplayed: users => users.every(u => this.authUser.canManage(u))
}, },
{ {
label: this.i18n('Ban'), label: this.i18n('Ban'),
description: this.i18n('Videos will be kept as private, comments will be kept as is.'),
handler: users => this.openBanUserModal(users), handler: users => this.openBanUserModal(users),
isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === false) isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === false)
}, },
@ -68,7 +71,9 @@ export class UserListComponent extends RestTable implements OnInit {
label: this.i18n('Unban'), label: this.i18n('Unban'),
handler: users => this.unbanUsers(users), handler: users => this.unbanUsers(users),
isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === true) isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === true)
}, }
],
[
{ {
label: this.i18n('Set Email as Verified'), label: this.i18n('Set Email as Verified'),
handler: users => this.setEmailsAsVerified(users), handler: users => this.setEmailsAsVerified(users),
@ -78,6 +83,7 @@ export class UserListComponent extends RestTable implements OnInit {
} }
} }
] ]
]
} }
openBanUserModal (users: User[]) { openBanUserModal (users: User[]) {

View File

@ -1,7 +1,7 @@
<div class="top-buttons"> <div class="top-buttons">
<div class="history-switch"> <div class="history-switch">
<p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch> <p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch>
<label i18n>Enable video history</label> <label i18n>Video history</label>
</div> </div>
<button class="delete-history" (click)="deleteHistory()" i18n> <button class="delete-history" (click)="deleteHistory()" i18n>

View File

@ -4,7 +4,7 @@ import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular
import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
import { is18nPath } from '../../../shared/models/i18n' import { is18nPath } from '../../../shared/models/i18n'
import { ScreenService } from '@app/shared/misc/screen.service' import { ScreenService } from '@app/shared/misc/screen.service'
import { debounceTime, filter, first, map, pairwise, skip, switchMap } from 'rxjs/operators' import { debounceTime, filter, map, pairwise } from 'rxjs/operators'
import { Hotkey, HotkeysService } from 'angular2-hotkeys' import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { fromEvent } from 'rxjs' import { fromEvent } from 'rxjs'

View File

@ -15,17 +15,23 @@
<ng-container *ngFor="let action of actions"> <ng-container *ngFor="let action of actions">
<ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
<a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)"> <ng-template #templateActionLabel let-action>
<my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon> <my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon>
{{ action.label }} <div class="d-flex flex-column">
<span i18n>{{ action.label }}</span>
<small class="text-muted" *ngIf="action.description">{{ action.description }}</small>
</div>
</ng-template>
<a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''">
<ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
</a> </a>
<span <span
*ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)" *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)"
class="custom-action dropdown-item" role="button" class="custom-action dropdown-item" role="button" [title]="action.title || ''"
> >
<my-global-icon *ngIf="action.iconName" [iconName]="action.iconName" [ngClass]="'icon-' + action.iconName"></my-global-icon> <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
{{ action.label }}
</span> </span>
</ng-container> </ng-container>

View File

@ -52,6 +52,7 @@
.dropdown-menu { .dropdown-menu {
.dropdown-item { .dropdown-item {
display: flex;
cursor: pointer; cursor: pointer;
color: #000 !important; color: #000 !important;

View File

@ -4,6 +4,8 @@ import { GlobalIconName } from '@app/shared/images/global-icon.component'
export type DropdownAction<T> = { export type DropdownAction<T> = {
label?: string label?: string
iconName?: GlobalIconName iconName?: GlobalIconName
description?: string
title?: string
handler?: (a: T) => any handler?: (a: T) => any
linkBuilder?: (a: T) => (string | number)[] linkBuilder?: (a: T) => (string | number)[]
isDisplayed?: (a: T) => boolean isDisplayed?: (a: T) => boolean

View File

@ -243,20 +243,24 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
if (this.user && authUser.hasRight(UserRight.MANAGE_USERS) && authUser.canManage(this.user)) { if (this.user && authUser.hasRight(UserRight.MANAGE_USERS) && authUser.canManage(this.user)) {
this.userActions.push([ this.userActions.push([
{ {
label: this.i18n('Edit user'), label: this.i18n('Edit'),
description: this.i18n('Change quota, role, and more.'),
linkBuilder: ({ user }) => this.getRouterUserEditLink(user) linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
}, },
{ {
label: this.i18n('Delete user'), label: this.i18n('Delete'),
description: this.i18n('Videos will be deleted, comments will be tombstoned.'),
handler: ({ user }) => this.removeUser(user) handler: ({ user }) => this.removeUser(user)
}, },
{ {
label: this.i18n('Ban user'), label: this.i18n('Ban'),
description: this.i18n('Videos will be kept as private, comments will be kept as is.'),
handler: ({ user }) => this.openBanUserModal(user), handler: ({ user }) => this.openBanUserModal(user),
isDisplayed: ({ user }) => !user.blocked isDisplayed: ({ user }) => !user.blocked
}, },
{ {
label: this.i18n('Unban user'), label: this.i18n('Unban user'),
description: this.i18n('Allow the user to login and create videos/comments again'),
handler: ({ user }) => this.unbanUser(user), handler: ({ user }) => this.unbanUser(user),
isDisplayed: ({ user }) => user.blocked isDisplayed: ({ user }) => user.blocked
}, },
@ -274,21 +278,25 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
this.userActions.push([ this.userActions.push([
{ {
label: this.i18n('Mute this account'), label: this.i18n('Mute this account'),
description: this.i18n('Hide any content from that user for you.'),
isDisplayed: ({ account }) => account.mutedByUser === false, isDisplayed: ({ account }) => account.mutedByUser === false,
handler: ({ account }) => this.blockAccountByUser(account) handler: ({ account }) => this.blockAccountByUser(account)
}, },
{ {
label: this.i18n('Unmute this account'), label: this.i18n('Unmute this account'),
description: this.i18n('Show back content from that user for you.'),
isDisplayed: ({ account }) => account.mutedByUser === true, isDisplayed: ({ account }) => account.mutedByUser === true,
handler: ({ account }) => this.unblockAccountByUser(account) handler: ({ account }) => this.unblockAccountByUser(account)
}, },
{ {
label: this.i18n('Mute the instance'), label: this.i18n('Mute the instance'),
description: this.i18n('Hide any content from that instance for you.'),
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
handler: ({ account }) => this.blockServerByUser(account.host) handler: ({ account }) => this.blockServerByUser(account.host)
}, },
{ {
label: this.i18n('Unmute the instance'), label: this.i18n('Unmute the instance'),
description: this.i18n('Show back content from that instance for you.'),
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
handler: ({ account }) => this.unblockServerByUser(account.host) handler: ({ account }) => this.unblockServerByUser(account.host)
} }
@ -301,11 +309,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
instanceActions = instanceActions.concat([ instanceActions = instanceActions.concat([
{ {
label: this.i18n('Mute this account by your instance'), label: this.i18n('Mute this account by your instance'),
description: this.i18n('Hide any content from that user for you, your instance and its users.'),
isDisplayed: ({ account }) => account.mutedByInstance === false, isDisplayed: ({ account }) => account.mutedByInstance === false,
handler: ({ account }) => this.blockAccountByInstance(account) handler: ({ account }) => this.blockAccountByInstance(account)
}, },
{ {
label: this.i18n('Unmute this account by your instance'), label: this.i18n('Unmute this account by your instance'),
description: this.i18n('Show back content from that user for you, your instance and its users.'),
isDisplayed: ({ account }) => account.mutedByInstance === true, isDisplayed: ({ account }) => account.mutedByInstance === true,
handler: ({ account }) => this.unblockAccountByInstance(account) handler: ({ account }) => this.unblockAccountByInstance(account)
} }
@ -317,11 +327,13 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
instanceActions = instanceActions.concat([ instanceActions = instanceActions.concat([
{ {
label: this.i18n('Mute the instance by your instance'), label: this.i18n('Mute the instance by your instance'),
description: this.i18n('Hide any content from that instance for you, your instance and its users.'),
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
handler: ({ account }) => this.blockServerByInstance(account.host) handler: ({ account }) => this.blockServerByInstance(account.host)
}, },
{ {
label: this.i18n('Unmute the instance by your instance'), label: this.i18n('Unmute the instance by your instance'),
description: this.i18n('Show back content from that instance for you, your instance and its users.'),
isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
handler: ({ account }) => this.unblockServerByInstance(account.host) handler: ({ account }) => this.unblockServerByInstance(account.host)
} }

View File

@ -1,5 +1,5 @@
<form novalidate [formGroup]="form" (ngSubmit)="formValidated()"> <form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
<div class="form-group"> <div class="form-group mb-2">
<input type="email" <input type="email"
formControlName="text" formControlName="text"
class="form-control" class="form-control"

View File

@ -54,7 +54,7 @@
<span *ngIf="isUserLoggedIn()" i18n>Subscribe with your local account</span> <span *ngIf="isUserLoggedIn()" i18n>Subscribe with your local account</span>
</button> </button>
<button class="dropdown-item" i18n>Subscribe with a Mastodon account:</button> <button class="dropdown-item dropdown-item-neutral" i18n>Subscribe with a Mastodon account:</button>
<my-remote-subscribe showHelp="true" [uri]="uri"></my-remote-subscribe> <my-remote-subscribe showHelp="true" [uri]="uri"></my-remote-subscribe>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>

View File

@ -69,6 +69,15 @@
button { button {
cursor: pointer; cursor: pointer;
} }
.dropdown-item-neutral {
cursor: default;
&:hover,
&:focus {
background-color: inherit;
}
}
} }
.dropdown-header { .dropdown-header {

View File

@ -10,14 +10,7 @@
tabindex="-1" tabindex="-1"
class="video-miniature-name" class="video-miniature-name"
[routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }"
> >{{ video.name }}</a>
<ng-container *ngIf="displayOptions.privacyLabel">
<span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span>
<span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span>
</ng-container>
{{ video.name }}
</a>
<span class="video-miniature-created-at-views"> <span class="video-miniature-created-at-views">
<my-date-toggle *ngIf="displayOptions.date" [date]="video.publishedAt"></my-date-toggle> <my-date-toggle *ngIf="displayOptions.date" [date]="video.publishedAt"></my-date-toggle>
@ -26,6 +19,11 @@
<ng-container *ngIf="displayOptions.date && displayOptions.views"></ng-container> <ng-container *ngIf="displayOptions.date && displayOptions.views"></ng-container>
<ng-container i18n *ngIf="displayOptions.views">{video.views, plural, =1 {1 view} other {{{ video.views | myNumberFormatter }} views}}</ng-container> <ng-container i18n *ngIf="displayOptions.views">{video.views, plural, =1 {1 view} other {{{ video.views | myNumberFormatter }} views}}</ng-container>
</span> </span>
<ng-container *ngIf="displayOptions.privacyLabel">
<span *ngIf="isUnlistedVideo()" class="badge badge-warning ml-1" i18n>Unlisted</span>
<span *ngIf="isPrivateVideo()" class="badge badge-danger ml-1" i18n>Private</span>
</ng-container>
</span> </span>
<a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">

View File

@ -374,6 +374,8 @@ p-tablecheckbox:hover div .ui-chkbox-box {
} }
p-inputswitch { p-inputswitch {
height: 26px;
.ui-inputswitch-checked .ui-inputswitch-slider { .ui-inputswitch-checked .ui-inputswitch-slider {
background-color: var(--mainColor) !important; background-color: var(--mainColor) !important;
} }