aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+accounts/accounts.component.html10
-rw-r--r--client/src/app/+accounts/accounts.component.scss8
-rw-r--r--client/src/app/+accounts/accounts.component.ts4
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html8
-rw-r--r--client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts76
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html9
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss12
-rw-r--r--client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts1
-rw-r--r--client/src/app/+admin/system/runners/runner.service.ts42
-rw-r--r--client/src/app/+video-channels/video-channels.component.html10
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss8
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts4
-rw-r--r--client/src/app/core/rest/rest.service.ts6
-rw-r--r--client/src/app/shared/shared-forms/advanced-input-filter.component.scss1
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.html11
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.ts4
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.html9
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.scss15
-rw-r--r--client/src/app/shared/shared-main/buttons/copy-button.component.ts22
-rw-r--r--client/src/app/shared/shared-main/buttons/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts4
21 files changed, 213 insertions, 52 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 66d108134..1283105e9 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -25,11 +25,11 @@
25 25
26 <div class="actor-handle"> 26 <div class="actor-handle">
27 <span>@{{ account.nameWithHost }}</span> 27 <span>@{{ account.nameWithHost }}</span>
28 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 28
29 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title 29 <my-copy-button
30 > 30 [value]="account.nameWithHostForced" i18n-notification notification="Username copied"
31 <my-global-icon iconName="copy"></my-global-icon> 31 title="Copy account handle" i18n-title
32 </button> 32 ></my-copy-button>
33 </div> 33 </div>
34 34
35 <div class="actor-counters"> 35 <div class="actor-counters">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 56b952b65..aadd6f5c0 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -28,14 +28,8 @@
28 } 28 }
29} 29}
30 30
31.copy-button { 31my-copy-button {
32 @include margin-left(3px); 32 @include margin-left(3px);
33
34 border: 0;
35
36 my-global-icon {
37 width: 15px;
38 }
39} 33}
40 34
41.account-info { 35.account-info {
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index 0033fbf59..6d912e325 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -115,10 +115,6 @@ export class AccountsComponent implements OnInit, OnDestroy {
115 this.redirectService.redirectToHomepage() 115 this.redirectService.redirectToHomepage()
116 } 116 }
117 117
118 activateCopiedMessage () {
119 this.notifier.success($localize`Username copied`)
120 }
121
122 searchChanged (search: string) { 118 searchChanged (search: string) {
123 const queryParams = { search } 119 const queryParams = { search }
124 120
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
index a759e9186..1a40da0c0 100644
--- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.html
@@ -31,7 +31,7 @@
31 <th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th> 31 <th style="width: 100px" i18n pSortableColumn="priority">Priority <p-sortIcon field="priority"></p-sortIcon></th>
32 <th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th> 32 <th style="width: 100px" i18n pSortableColumn="progress">Progress <p-sortIcon field="progress"></p-sortIcon></th>
33 <th i18n>Runner</th> 33 <th i18n>Runner</th>
34 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 34 <th style="width: 200px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
35 </tr> 35 </tr>
36 </ng-template> 36 </ng-template>
37 37
@@ -47,7 +47,7 @@
47 </div> 47 </div>
48 48
49 <div class="ms-auto d-flex"> 49 <div class="ms-auto d-flex">
50 <my-advanced-input-filter class="me-2" (search)="onSearch($event)"></my-advanced-input-filter> 50 <my-advanced-input-filter class="me-2" [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter>
51 51
52 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button> 52 <my-button i18n-label label="Refresh" icon="refresh" (click)="reloadData()"></my-button>
53 </div> 53 </div>
@@ -73,7 +73,9 @@
73 73
74 <td>{{ runnerJob.uuid }}</td> 74 <td>{{ runnerJob.uuid }}</td>
75 <td>{{ runnerJob.type }}</td> 75 <td>{{ runnerJob.type }}</td>
76 <td>{{ runnerJob.state.label }}</td> 76 <td>
77 <span class="pt-badge" [ngClass]="getStateBadgeColor(runnerJob)">{{ runnerJob.state.label }}</span>
78 </td>
77 <td>{{ runnerJob.priority }}</td> 79 <td>{{ runnerJob.priority }}</td>
78 80
79 <td> 81 <td>
diff --git a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
index 8994c1d00..2670eac86 100644
--- a/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
+++ b/client/src/app/+admin/system/runners/runner-job-list/runner-job-list.component.ts
@@ -5,6 +5,7 @@ import { formatICU } from '@app/helpers'
5import { DropdownAction } from '@app/shared/shared-main' 5import { DropdownAction } from '@app/shared/shared-main'
6import { RunnerJob, RunnerJobState } from '@shared/models' 6import { RunnerJob, RunnerJobState } from '@shared/models'
7import { RunnerJobFormatted, RunnerService } from '../runner.service' 7import { RunnerJobFormatted, RunnerService } from '../runner.service'
8import { AdvancedInputFilter } from '@app/shared/shared-forms'
8 9
9@Component({ 10@Component({
10 selector: 'my-runner-job-list', 11 selector: 'my-runner-job-list',
@@ -20,6 +21,30 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
20 actions: DropdownAction<RunnerJob>[][] = [] 21 actions: DropdownAction<RunnerJob>[][] = []
21 bulkActions: DropdownAction<RunnerJob[]>[][] = [] 22 bulkActions: DropdownAction<RunnerJob[]>[][] = []
22 23
24 inputFilters: AdvancedInputFilter[] = [
25 {
26 title: $localize`Advanced filters`,
27 children: [
28 {
29 value: 'state:completed',
30 label: $localize`Completed jobs`
31 },
32 {
33 value: 'state:pending state:waiting-for-parent-job',
34 label: $localize`Pending jobs`
35 },
36 {
37 value: 'state:processing',
38 label: $localize`Jobs that are being processed`
39 },
40 {
41 value: 'state:errored state:parent-errored',
42 label: $localize`Failed jobs`
43 }
44 ]
45 }
46 ]
47
23 constructor ( 48 constructor (
24 private runnerService: RunnerService, 49 private runnerService: RunnerService,
25 private notifier: Notifier, 50 private notifier: Notifier,
@@ -36,6 +61,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
36 handler: job => this.cancelJobs([ job ]), 61 handler: job => this.cancelJobs([ job ]),
37 isDisplayed: job => this.canCancelJob(job) 62 isDisplayed: job => this.canCancelJob(job)
38 } 63 }
64 ],
65 [
66 {
67 label: $localize`Delete this job`,
68 handler: job => this.removeJobs([ job ])
69 }
39 ] 70 ]
40 ] 71 ]
41 72
@@ -46,6 +77,12 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
46 handler: jobs => this.cancelJobs(jobs), 77 handler: jobs => this.cancelJobs(jobs),
47 isDisplayed: jobs => jobs.every(j => this.canCancelJob(j)) 78 isDisplayed: jobs => jobs.every(j => this.canCancelJob(j))
48 } 79 }
80 ],
81 [
82 {
83 label: $localize`Delete`,
84 handler: jobs => this.removeJobs(jobs)
85 }
49 ] 86 ]
50 ] 87 ]
51 88
@@ -77,6 +114,45 @@ export class RunnerJobListComponent extends RestTable <RunnerJob> implements OnI
77 }) 114 })
78 } 115 }
79 116
117 async removeJobs (jobs: RunnerJob[]) {
118 const message = formatICU(
119 $localize`Do you really want to remove {count, plural, =1 {this job} other {{count} jobs}}? Children jobs will also be removed.`,
120 { count: jobs.length }
121 )
122
123 const res = await this.confirmService.confirm(message, $localize`Remove`)
124
125 if (res === false) return
126
127 this.runnerService.removeJobs(jobs)
128 .subscribe({
129 next: () => {
130 this.reloadData()
131 this.notifier.success($localize`Job(s) removed.`)
132 },
133
134 error: err => this.notifier.error(err.message)
135 })
136 }
137
138 getStateBadgeColor (job: RunnerJob) {
139 switch (job.state.id) {
140 case RunnerJobState.ERRORED:
141 case RunnerJobState.PARENT_ERRORED:
142 return 'badge-danger'
143
144 case RunnerJobState.COMPLETED:
145 return 'badge-success'
146
147 case RunnerJobState.PENDING:
148 case RunnerJobState.WAITING_FOR_PARENT_JOB:
149 return 'badge-warning'
150
151 default:
152 return 'badge-info'
153 }
154 }
155
80 protected reloadDataInternal () { 156 protected reloadDataInternal () {
81 this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search }) 157 this.runnerService.listRunnerJobs({ pagination: this.pagination, sort: this.sort, search: this.search })
82 .subscribe({ 158 .subscribe({
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
index 3e5cea881..4ff2c3b4c 100644
--- a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.html
@@ -45,7 +45,14 @@
45 ></my-action-dropdown> 45 ></my-action-dropdown>
46 </td> 46 </td>
47 47
48 <td>{{ registrationToken.registrationToken }}</td> 48 <td>
49 {{ registrationToken.registrationToken }}
50
51 <my-copy-button
52 [value]="registrationToken.registrationToken" i18n-notification notification="Registration token copied"
53 i18n-title title="Copy registration token"
54 ></my-copy-button>
55 </td>
49 56
50 <td>{{ registrationToken.createdAt | date: 'short' }}</td> 57 <td>{{ registrationToken.createdAt | date: 'short' }}</td>
51 58
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss
new file mode 100644
index 000000000..1cfb2e65f
--- /dev/null
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.scss
@@ -0,0 +1,12 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4my-copy-button {
5 @include margin-left(3px);
6}
7
8tr:not(:hover) {
9 my-copy-button {
10 opacity: 0;
11 }
12}
diff --git a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
index f03aab189..77908a2e1 100644
--- a/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
+++ b/client/src/app/+admin/system/runners/runner-registration-token-list/runner-registration-token-list.component.ts
@@ -7,6 +7,7 @@ import { RunnerService } from '../runner.service'
7 7
8@Component({ 8@Component({
9 selector: 'my-runner-registration-token-list', 9 selector: 'my-runner-registration-token-list',
10 styleUrls: [ './runner-registration-token-list.component.scss' ],
10 templateUrl: './runner-registration-token-list.component.html' 11 templateUrl: './runner-registration-token-list.component.html'
11}) 12})
12export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit { 13export class RunnerRegistrationTokenListComponent extends RestTable <RunnerRegistrationToken> implements OnInit {
diff --git a/client/src/app/+admin/system/runners/runner.service.ts b/client/src/app/+admin/system/runners/runner.service.ts
index 392ec82bc..3ab36c4ff 100644
--- a/client/src/app/+admin/system/runners/runner.service.ts
+++ b/client/src/app/+admin/system/runners/runner.service.ts
@@ -6,7 +6,7 @@ import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core' 6import { RestExtractor, RestPagination, RestService, ServerService } from '@app/core'
7import { arrayify, peertubeTranslate } from '@shared/core-utils' 7import { arrayify, peertubeTranslate } from '@shared/core-utils'
8import { ResultList } from '@shared/models/common' 8import { ResultList } from '@shared/models/common'
9import { Runner, RunnerJob, RunnerJobAdmin, RunnerRegistrationToken } from '@shared/models/runners' 9import { Runner, RunnerJob, RunnerJobAdmin, RunnerJobState, RunnerRegistrationToken } from '@shared/models/runners'
10import { environment } from '../../../../environments/environment' 10import { environment } from '../../../../environments/environment'
11 11
12export type RunnerJobFormatted = RunnerJob & { 12export type RunnerJobFormatted = RunnerJob & {
@@ -60,7 +60,9 @@ export class RunnerService {
60 let params = new HttpParams() 60 let params = new HttpParams()
61 params = this.restService.addRestGetParams(params, pagination, sort) 61 params = this.restService.addRestGetParams(params, pagination, sort)
62 62
63 if (search) params = params.append('search', search) 63 if (search) {
64 params = this.buildParamsFromSearch(search, params)
65 }
64 66
65 return forkJoin([ 67 return forkJoin([
66 this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }), 68 this.authHttp.get<ResultList<RunnerJobAdmin>>(RunnerService.BASE_RUNNER_URL + '/jobs', { params }),
@@ -90,6 +92,31 @@ export class RunnerService {
90 ) 92 )
91 } 93 }
92 94
95 private buildParamsFromSearch (search: string, params: HttpParams) {
96 const filters = this.restService.parseQueryStringFilter(search, {
97 stateOneOf: {
98 prefix: 'state:',
99 multiple: true,
100 handler: v => {
101 if (v === 'completed') return RunnerJobState.COMPLETED
102 if (v === 'processing') return RunnerJobState.PROCESSING
103 if (v === 'errored') return RunnerJobState.ERRORED
104 if (v === 'pending') return RunnerJobState.PENDING
105 if (v === 'waiting-for-parent-job') return RunnerJobState.WAITING_FOR_PARENT_JOB
106 if (v === 'parent-errored') return RunnerJobState.PARENT_ERRORED
107
108 return undefined
109 }
110 }
111 })
112
113 console.log(filters)
114
115 return this.restService.addObjectParams(params, filters)
116 }
117
118 // ---------------------------------------------------------------------------
119
93 cancelJobs (jobsArg: RunnerJob | RunnerJob[]) { 120 cancelJobs (jobsArg: RunnerJob | RunnerJob[]) {
94 const jobs = arrayify(jobsArg) 121 const jobs = arrayify(jobsArg)
95 122
@@ -101,6 +128,17 @@ export class RunnerService {
101 ) 128 )
102 } 129 }
103 130
131 removeJobs (jobsArg: RunnerJob | RunnerJob[]) {
132 const jobs = arrayify(jobsArg)
133
134 return from(jobs)
135 .pipe(
136 concatMap(job => this.authHttp.delete(RunnerService.BASE_RUNNER_URL + '/jobs/' + job.uuid)),
137 toArray(),
138 catchError(err => this.restExtractor.handleError(err))
139 )
140 }
141
104 // --------------------------------------------------------------------------- 142 // ---------------------------------------------------------------------------
105 143
106 listRunners (options: { 144 listRunners (options: {
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index fff160f2e..228cc4edd 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -64,11 +64,11 @@
64 64
65 <div class="actor-handle"> 65 <div class="actor-handle">
66 <span>@{{ videoChannel.nameWithHost }}</span> 66 <span>@{{ videoChannel.nameWithHost }}</span>
67 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 67
68 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title 68 <my-copy-button
69 > 69 [value]="videoChannel.nameWithHostForced" i18n-notification notification="Handle copied"
70 <my-global-icon iconName="copy"></my-global-icon> 70 title="Copy channel handle" i18n-title
71 </button> 71 ></my-copy-button>
72 </div> 72 </div>
73 73
74 <div class="actor-counters"> 74 <div class="actor-counters">
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index aba266fcc..182e8d845 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -152,14 +152,8 @@
152 display: none; 152 display: none;
153} 153}
154 154
155.copy-button { 155my-copy-button {
156 @include margin-left(3px); 156 @include margin-left(3px);
157
158 border: 0;
159
160 my-global-icon {
161 width: 15px;
162 }
163} 157}
164 158
165@media screen and (max-width: 1400px) { 159@media screen and (max-width: 1400px) {
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index afbf96032..f5bea66ec 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -120,10 +120,6 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
120 return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) 120 return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
121 } 121 }
122 122
123 activateCopiedMessage () {
124 this.notifier.success($localize`Username copied`)
125 }
126
127 hasShowMoreDescription () { 123 hasShowMoreDescription () {
128 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100 124 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
129 } 125 }
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts
index d8b5ffb18..f07afb7e8 100644
--- a/client/src/app/core/rest/rest.service.ts
+++ b/client/src/app/core/rest/rest.service.ts
@@ -7,16 +7,18 @@ import { RestPagination } from './rest-pagination'
7 7
8const debugLogger = debug('peertube:rest') 8const debugLogger = debug('peertube:rest')
9 9
10type ParseQueryHandlerResult = string | number | boolean | string[] | number[] | boolean[]
11
10interface QueryStringFilterPrefixes { 12interface QueryStringFilterPrefixes {
11 [key: string]: { 13 [key: string]: {
12 prefix: string 14 prefix: string
13 handler?: (v: string) => string | number | boolean 15 handler?: (v: string) => ParseQueryHandlerResult
14 multiple?: boolean 16 multiple?: boolean
15 isBoolean?: boolean 17 isBoolean?: boolean
16 } 18 }
17} 19}
18 20
19type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, string | number | boolean | (string | number | boolean)[]>> 21type ParseQueryStringFilters <K extends keyof any> = Partial<Record<K, ParseQueryHandlerResult | ParseQueryHandlerResult[]>>
20type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string } 22type ParseQueryStringFiltersResult <K extends keyof any> = ParseQueryStringFilters<K> & { search?: string }
21 23
22@Injectable() 24@Injectable()
diff --git a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
index 4efbeb85d..f5438ffdb 100644
--- a/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
+++ b/client/src/app/shared/shared-forms/advanced-input-filter.component.scss
@@ -33,6 +33,5 @@ my-global-icon {
33 33
34div[role=menu] { 34div[role=menu] {
35 max-height: 50vh; 35 max-height: 50vh;
36 min-height: 200px;
37 overflow: auto; 36 overflow: auto;
38} 37}
diff --git a/client/src/app/shared/shared-forms/input-text.component.html b/client/src/app/shared/shared-forms/input-text.component.html
index abb53a085..4747e2f8f 100644
--- a/client/src/app/shared/shared-forms/input-text.component.html
+++ b/client/src/app/shared/shared-forms/input-text.component.html
@@ -11,13 +11,12 @@
11 <my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon> 11 <my-global-icon *ngIf="!show" iconName="eye-close"></my-global-icon>
12 </button> 12 </button>
13 13
14 <button 14 <my-copy-button
15 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 15 *ngIf="withCopy" [value]="input.value" i18n-notification notification="Copied"
16 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy" 16 [isInputGroup]="true" i18n
17 > 17 >
18 <my-global-icon iconName="copy"></my-global-icon> 18 COPY
19 <span class="copy-text">Copy</span> 19 </my-copy-button>
20 </button>
21</div> 20</div>
22 21
23<div *ngIf="formError" class="form-error">{{ formError }}</div> 22<div *ngIf="formError" class="form-error">{{ formError }}</div>
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts
index aa4a1cba8..be03f25b9 100644
--- a/client/src/app/shared/shared-forms/input-text.component.ts
+++ b/client/src/app/shared/shared-forms/input-text.component.ts
@@ -46,10 +46,6 @@ export class InputTextComponent implements ControlValueAccessor {
46 this.show = !this.show 46 this.show = !this.show
47 } 47 }
48 48
49 activateCopiedMessage () {
50 this.notifier.success($localize`Copied`)
51 }
52
53 propagateChange = (_: any) => { /* empty */ } 49 propagateChange = (_: any) => { /* empty */ }
54 50
55 writeValue (value: string) { 51 writeValue (value: string) {
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.html b/client/src/app/shared/shared-main/buttons/copy-button.component.html
new file mode 100644
index 000000000..a99c0a93a
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.html
@@ -0,0 +1,9 @@
1<button
2 class="btn btn-outline-secondary btn-sm copy-button"
3 [cdkCopyToClipboard]="value" (click)="activateCopiedMessage()"
4 [title]="title" [ngClass]="{ 'is-input-group': isInputGroup }"
5>
6 <my-global-icon iconName="copy"></my-global-icon>
7
8 <ng-content></ng-content>
9</button>
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.scss b/client/src/app/shared/shared-main/buttons/copy-button.component.scss
new file mode 100644
index 000000000..7e3720418
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.scss
@@ -0,0 +1,15 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4button:not(.is-input-group) {
5 border: 0;
6}
7
8.is-input-group {
9 border-top-left-radius: 0;
10 border-bottom-left-radius: 0;
11}
12
13my-global-icon {
14 width: 15px;
15}
diff --git a/client/src/app/shared/shared-main/buttons/copy-button.component.ts b/client/src/app/shared/shared-main/buttons/copy-button.component.ts
new file mode 100644
index 000000000..aac9ab8b0
--- /dev/null
+++ b/client/src/app/shared/shared-main/buttons/copy-button.component.ts
@@ -0,0 +1,22 @@
1import { Component, Input } from '@angular/core'
2import { Notifier } from '@app/core'
3
4@Component({
5 selector: 'my-copy-button',
6 styleUrls: [ './copy-button.component.scss' ],
7 templateUrl: './copy-button.component.html'
8})
9export class CopyButtonComponent {
10 @Input() value: string
11 @Input() title: string
12 @Input() notification: string
13 @Input() isInputGroup = false
14
15 constructor (private notifier: Notifier) {
16
17 }
18
19 activateCopiedMessage () {
20 if (this.notification) this.notifier.success(this.notification)
21 }
22}
diff --git a/client/src/app/shared/shared-main/buttons/index.ts b/client/src/app/shared/shared-main/buttons/index.ts
index 775a47a39..75efbdea3 100644
--- a/client/src/app/shared/shared-main/buttons/index.ts
+++ b/client/src/app/shared/shared-main/buttons/index.ts
@@ -1,4 +1,5 @@
1export * from './action-dropdown.component' 1export * from './action-dropdown.component'
2export * from './button.component' 2export * from './button.component'
3export * from './copy-button.component'
3export * from './delete-button.component' 4export * from './delete-button.component'
4export * from './edit-button.component' 5export * from './edit-button.component'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 480277450..243394bda 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -31,7 +31,7 @@ import {
31 PeerTubeTemplateDirective 31 PeerTubeTemplateDirective
32} from './angular' 32} from './angular'
33import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 33import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
34import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' 34import { ActionDropdownComponent, ButtonComponent, CopyButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
35import { CustomPageService } from './custom-page' 35import { CustomPageService } from './custom-page'
36import { DateToggleComponent } from './date' 36import { DateToggleComponent } from './date'
37import { FeedComponent } from './feeds' 37import { FeedComponent } from './feeds'
@@ -100,6 +100,7 @@ import { VideoChannelService } from './video-channel'
100 100
101 ActionDropdownComponent, 101 ActionDropdownComponent,
102 ButtonComponent, 102 ButtonComponent,
103 CopyButtonComponent,
103 DeleteButtonComponent, 104 DeleteButtonComponent,
104 EditButtonComponent, 105 EditButtonComponent,
105 106
@@ -162,6 +163,7 @@ import { VideoChannelService } from './video-channel'
162 163
163 ActionDropdownComponent, 164 ActionDropdownComponent,
164 ButtonComponent, 165 ButtonComponent,
166 CopyButtonComponent,
165 DeleteButtonComponent, 167 DeleteButtonComponent,
166 EditButtonComponent, 168 EditButtonComponent,
167 169