@chisquare-tech/origine-ngx-apps-code 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,2969 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, signal, computed, effect, untracked, Directive, ElementRef, input, output, HostListener, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { ApiService } from '@thescaffold/ngx-core';
|
|
4
|
+
import { map, forkJoin, of } from 'rxjs';
|
|
5
|
+
import * as i1 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import { ActivatedRoute, RouterLink, Router } from '@angular/router';
|
|
8
|
+
import { map as map$1, catchError } from 'rxjs/operators';
|
|
9
|
+
import { marked } from 'marked';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* What the Code screens need from the app that mounts them (cloud/web provides it, as it provides
|
|
13
|
+
* DesignHost): where the person is, who is in the organization, and links inside the project.
|
|
14
|
+
*/
|
|
15
|
+
class CodeHost {
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const C = 'v1/code';
|
|
19
|
+
const enc = encodeURIComponent;
|
|
20
|
+
const data = (r) => r?.data;
|
|
21
|
+
function apiMessage(err, fallback = 'Something went wrong. Try again.') {
|
|
22
|
+
return err?.error?.message || err?.body?.message || err?.message || fallback;
|
|
23
|
+
}
|
|
24
|
+
class CodeApi {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.api = inject(ApiService);
|
|
27
|
+
}
|
|
28
|
+
repoPath(project, name) {
|
|
29
|
+
return `${C}/project/${enc(project)}/repo/${enc(name)}`;
|
|
30
|
+
}
|
|
31
|
+
repos(project) {
|
|
32
|
+
return this.api.get(`${C}/project/${enc(project)}/repo`).pipe(map((r) => data(r) ?? []));
|
|
33
|
+
}
|
|
34
|
+
create(project, body) {
|
|
35
|
+
return this.api.post(`${C}/project/${enc(project)}/repo`, body).pipe(map((r) => data(r)));
|
|
36
|
+
}
|
|
37
|
+
import(project, body) {
|
|
38
|
+
return this.api.post(`${C}/project/${enc(project)}/repo/import`, body).pipe(map((r) => data(r)));
|
|
39
|
+
}
|
|
40
|
+
repo(project, name) {
|
|
41
|
+
return this.api.get(this.repoPath(project, name)).pipe(map((r) => data(r)));
|
|
42
|
+
}
|
|
43
|
+
update(project, name, body) {
|
|
44
|
+
return this.api.patch(this.repoPath(project, name), body).pipe(map((r) => data(r)));
|
|
45
|
+
}
|
|
46
|
+
remove(project, name) {
|
|
47
|
+
return this.api.delete(`${this.repoPath(project, name)}?confirm=${enc(name)}`);
|
|
48
|
+
}
|
|
49
|
+
setArchived(project, name, on) {
|
|
50
|
+
return this.api.post(`${this.repoPath(project, name)}/${on ? 'archive' : 'unarchive'}`, {}).pipe(map((r) => data(r)));
|
|
51
|
+
}
|
|
52
|
+
protections(project, name) {
|
|
53
|
+
return this.api.get(`${this.repoPath(project, name)}/protection`).pipe(map((r) => data(r) ?? []));
|
|
54
|
+
}
|
|
55
|
+
addProtection(project, name, p) {
|
|
56
|
+
return this.api.post(`${this.repoPath(project, name)}/protection`, p).pipe(map((r) => data(r)));
|
|
57
|
+
}
|
|
58
|
+
updateProtection(project, name, id, p) {
|
|
59
|
+
return this.api.patch(`${this.repoPath(project, name)}/protection/${enc(id)}`, p).pipe(map((r) => data(r)));
|
|
60
|
+
}
|
|
61
|
+
removeProtection(project, name, id) {
|
|
62
|
+
return this.api.delete(`${this.repoPath(project, name)}/protection/${enc(id)}`);
|
|
63
|
+
}
|
|
64
|
+
tree(project, name, ref, path) {
|
|
65
|
+
return this.api.get(`${this.repoPath(project, name)}/tree`, { ref, path }).pipe(map((r) => data(r)));
|
|
66
|
+
}
|
|
67
|
+
blob(project, name, ref, path) {
|
|
68
|
+
return this.api.get(`${this.repoPath(project, name)}/blob`, { ref, path }).pipe(map((r) => data(r)));
|
|
69
|
+
}
|
|
70
|
+
blame(project, name, ref, path) {
|
|
71
|
+
return this.api.get(`${this.repoPath(project, name)}/blame`, { ref, path }).pipe(map((r) => data(r) ?? []));
|
|
72
|
+
}
|
|
73
|
+
files(project, name, ref) {
|
|
74
|
+
return this.api.get(`${this.repoPath(project, name)}/files`, { ref }).pipe(map((r) => data(r) ?? { files: [], truncated: false }));
|
|
75
|
+
}
|
|
76
|
+
languages(project, name, ref) {
|
|
77
|
+
return this.api.get(`${this.repoPath(project, name)}/languages`, { ref }).pipe(map((r) => data(r) ?? []));
|
|
78
|
+
}
|
|
79
|
+
archiveUrl(project, name, ref, format) {
|
|
80
|
+
return this.api.get(`${this.repoPath(project, name)}/archive`, { ref, format }).pipe(map((r) => data(r)?.url ?? ''));
|
|
81
|
+
}
|
|
82
|
+
commits(project, name, q) {
|
|
83
|
+
const p = {};
|
|
84
|
+
if (q.ref)
|
|
85
|
+
p['ref'] = q.ref;
|
|
86
|
+
if (q.path)
|
|
87
|
+
p['path'] = q.path;
|
|
88
|
+
if (q.limit)
|
|
89
|
+
p['limit'] = String(q.limit);
|
|
90
|
+
if (q.skip)
|
|
91
|
+
p['skip'] = String(q.skip);
|
|
92
|
+
return this.api.get(`${this.repoPath(project, name)}/commits`, p).pipe(map((r) => data(r) ?? []));
|
|
93
|
+
}
|
|
94
|
+
commit(project, name, sha) {
|
|
95
|
+
return this.api.get(`${this.repoPath(project, name)}/commit/${enc(sha)}`).pipe(map((r) => data(r)));
|
|
96
|
+
}
|
|
97
|
+
/** One file's hunks from base to head ("" base: the head's first parent). */
|
|
98
|
+
diff(project, name, head, path, base = '') {
|
|
99
|
+
const p = { head, path };
|
|
100
|
+
if (base)
|
|
101
|
+
p['base'] = base;
|
|
102
|
+
return this.api.get(`${this.repoPath(project, name)}/diff`, p).pipe(map((r) => data(r)));
|
|
103
|
+
}
|
|
104
|
+
compare(project, name, base, head) {
|
|
105
|
+
return this.api.get(`${this.repoPath(project, name)}/compare`, { base, head }).pipe(map((r) => data(r)));
|
|
106
|
+
}
|
|
107
|
+
refs(project, name, kind = 'branch', q = '') {
|
|
108
|
+
const p = { kind };
|
|
109
|
+
if (q)
|
|
110
|
+
p['q'] = q;
|
|
111
|
+
return this.api.get(`${this.repoPath(project, name)}/refs`, p).pipe(map((r) => data(r) ?? []));
|
|
112
|
+
}
|
|
113
|
+
createBranch(project, name, branch, from) {
|
|
114
|
+
return this.api.post(`${this.repoPath(project, name)}/branches`, { name: branch, from });
|
|
115
|
+
}
|
|
116
|
+
deleteBranch(project, name, branch) {
|
|
117
|
+
return this.api.delete(`${this.repoPath(project, name)}/branches?branch=${enc(branch)}`);
|
|
118
|
+
}
|
|
119
|
+
deleteMerged(project, name) {
|
|
120
|
+
return this.api.post(`${this.repoPath(project, name)}/branches/delete-merged`, {}).pipe(map((r) => data(r)));
|
|
121
|
+
}
|
|
122
|
+
pulls(project, name, q = {}) {
|
|
123
|
+
const p = {};
|
|
124
|
+
for (const [k, v] of Object.entries(q))
|
|
125
|
+
if (v !== undefined && v !== '' && v !== false)
|
|
126
|
+
p[k] = String(v);
|
|
127
|
+
return this.api.get(`${this.repoPath(project, name)}/pulls`, p).pipe(map((r) => data(r) ?? []));
|
|
128
|
+
}
|
|
129
|
+
template(project, name, target) {
|
|
130
|
+
return this.api.get(`${this.repoPath(project, name)}/pull-template`, { target }).pipe(map((r) => {
|
|
131
|
+
const d = data(r);
|
|
132
|
+
return typeof d === 'string' ? d : d?.body ?? '';
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
openPull(project, name, body) {
|
|
136
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls`, body).pipe(map((r) => data(r)));
|
|
137
|
+
}
|
|
138
|
+
pull(project, name, n) {
|
|
139
|
+
return this.api.get(`${this.repoPath(project, name)}/pulls/${n}`).pipe(map((r) => data(r)));
|
|
140
|
+
}
|
|
141
|
+
updatePull(project, name, n, body) {
|
|
142
|
+
return this.api.patch(`${this.repoPath(project, name)}/pulls/${n}`, body).pipe(map((r) => data(r)));
|
|
143
|
+
}
|
|
144
|
+
closePull(project, name, n, reopen = false) {
|
|
145
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/${reopen ? 'reopen' : 'close'}`, {}).pipe(map((r) => data(r)));
|
|
146
|
+
}
|
|
147
|
+
merge(project, name, n, body) {
|
|
148
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/merge`, body).pipe(map((r) => data(r)));
|
|
149
|
+
}
|
|
150
|
+
conversation(project, name, n) {
|
|
151
|
+
return this.api.get(`${this.repoPath(project, name)}/pulls/${n}/conversation`).pipe(map((r) => data(r) ?? { comments: [], reviews: [], threads: [] }));
|
|
152
|
+
}
|
|
153
|
+
comment(project, name, n, c) {
|
|
154
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/comments`, c).pipe(map((r) => data(r)));
|
|
155
|
+
}
|
|
156
|
+
review(project, name, n, state, body = '') {
|
|
157
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/reviews`, { state, body }).pipe(map((r) => data(r)));
|
|
158
|
+
}
|
|
159
|
+
resolve(project, name, n, thread, on) {
|
|
160
|
+
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/threads/${enc(thread)}/${on ? 'resolve' : 'unresolve'}`, {});
|
|
161
|
+
}
|
|
162
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CodeApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
163
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CodeApi, providedIn: 'root' }); }
|
|
164
|
+
}
|
|
165
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CodeApi, decorators: [{
|
|
166
|
+
type: Injectable,
|
|
167
|
+
args: [{ providedIn: 'root' }]
|
|
168
|
+
}] });
|
|
169
|
+
|
|
170
|
+
// Small helpers the Code screens share.
|
|
171
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
172
|
+
function shortDate(d) {
|
|
173
|
+
if (!d)
|
|
174
|
+
return '';
|
|
175
|
+
const t = new Date(d);
|
|
176
|
+
return Number.isNaN(t.getTime()) ? '' : `${MONTHS[t.getMonth()]} ${t.getDate()}`;
|
|
177
|
+
}
|
|
178
|
+
/** "Nov 7, 2026" for a day heading. */
|
|
179
|
+
function dayLabel(d) {
|
|
180
|
+
if (!d)
|
|
181
|
+
return '';
|
|
182
|
+
const t = new Date(d);
|
|
183
|
+
return Number.isNaN(t.getTime()) ? '' : `${MONTHS[t.getMonth()]} ${t.getDate()}, ${t.getFullYear()}`;
|
|
184
|
+
}
|
|
185
|
+
function ago(iso, now = Date.now()) {
|
|
186
|
+
if (!iso)
|
|
187
|
+
return '';
|
|
188
|
+
const t = Date.parse(iso);
|
|
189
|
+
if (Number.isNaN(t))
|
|
190
|
+
return '';
|
|
191
|
+
const s = Math.max(0, Math.round((now - t) / 1000));
|
|
192
|
+
if (s < 60)
|
|
193
|
+
return 'just now';
|
|
194
|
+
if (s < 3600)
|
|
195
|
+
return `${Math.floor(s / 60)} min ago`;
|
|
196
|
+
if (s < 86400)
|
|
197
|
+
return `${Math.floor(s / 3600)} h ago`;
|
|
198
|
+
if (s < 172800)
|
|
199
|
+
return 'Yesterday';
|
|
200
|
+
return shortDate(iso);
|
|
201
|
+
}
|
|
202
|
+
function initials(name) {
|
|
203
|
+
const w = (name || '').split(/[\s@._-]+/).filter(Boolean);
|
|
204
|
+
return ((w[0]?.[0] ?? '?') + (w[1]?.[0] ?? '')).toUpperCase();
|
|
205
|
+
}
|
|
206
|
+
function size(bytes) {
|
|
207
|
+
if (!bytes)
|
|
208
|
+
return '0 B';
|
|
209
|
+
if (bytes < 1024)
|
|
210
|
+
return `${bytes} B`;
|
|
211
|
+
if (bytes < 1024 * 1024)
|
|
212
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
213
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
214
|
+
}
|
|
215
|
+
/** Names and initials by user id, from the host's people. */
|
|
216
|
+
class People {
|
|
217
|
+
constructor(people) {
|
|
218
|
+
this.byId = new Map(people.map((p) => [p.id, p]));
|
|
219
|
+
}
|
|
220
|
+
name(id) {
|
|
221
|
+
if (!id)
|
|
222
|
+
return 'Someone';
|
|
223
|
+
return this.byId.get(id)?.name || this.byId.get(id)?.email || 'Someone';
|
|
224
|
+
}
|
|
225
|
+
initials(id) {
|
|
226
|
+
return initials(this.name(id));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const LANG_COLOURS = ['var(--o-acc)', 'var(--o-ok)', 'var(--o-run)', 'var(--o-vio)', 'var(--o-bad-solid)', 'var(--o-line-mid)'];
|
|
230
|
+
function langColour(i) {
|
|
231
|
+
return LANG_COLOURS[Math.min(i, LANG_COLOURS.length - 1)];
|
|
232
|
+
}
|
|
233
|
+
/** The state of checks as a dot class: success green, failure red, pending amber, none grey. */
|
|
234
|
+
function checkDot(state) {
|
|
235
|
+
return state === 'success' ? 'o-dot o-grn' : state === 'failure' || state === 'error' ? 'o-dot o-red' : state === 'pending' ? 'o-dot o-amb' : 'o-dot o-gry';
|
|
236
|
+
}
|
|
237
|
+
function copy(text) {
|
|
238
|
+
return navigator.clipboard?.writeText(text) ?? Promise.resolve();
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The repository the person was last in, per project, so the project-level tabs (Files, Commits,
|
|
242
|
+
* Branches, Settings) open it. Browser storage is a convenience: without it the first repository is used.
|
|
243
|
+
*/
|
|
244
|
+
class CurrentRepo {
|
|
245
|
+
get(project) {
|
|
246
|
+
try {
|
|
247
|
+
return localStorage.getItem(`origine.code.repo.${project}`) ?? '';
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
set(project, repo) {
|
|
254
|
+
try {
|
|
255
|
+
localStorage.setItem(`origine.code.repo.${project}`, repo);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
/* storage blocked: the first repository is used */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CurrentRepo, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
262
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CurrentRepo, providedIn: 'root' }); }
|
|
263
|
+
}
|
|
264
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CurrentRepo, decorators: [{
|
|
265
|
+
type: Injectable,
|
|
266
|
+
args: [{ providedIn: 'root' }]
|
|
267
|
+
}] });
|
|
268
|
+
/**
|
|
269
|
+
* Highlighted code from the server: spans with Chroma's short token classes behind "tk-" (tk-k a
|
|
270
|
+
* keyword, tk-s a string…), coloured with the theme's syntax tokens. Server HTML is not under the
|
|
271
|
+
* component's encapsulation, hence ::ng-deep.
|
|
272
|
+
*/
|
|
273
|
+
const TOKEN_CSS = `
|
|
274
|
+
:host ::ng-deep [class^="tk-k"]{color:var(--o-tk-k)}
|
|
275
|
+
:host ::ng-deep [class^="tk-s"], :host ::ng-deep [class^="tk-l"]{color:var(--o-tk-s)}
|
|
276
|
+
:host ::ng-deep [class^="tk-c"]{color:var(--o-tk-c);font-style:italic}
|
|
277
|
+
:host ::ng-deep [class^="tk-m"]{color:var(--o-tk-n)}
|
|
278
|
+
:host ::ng-deep .tk-nf, :host ::ng-deep .tk-nx, :host ::ng-deep .tk-fm{color:var(--o-tk-f)}
|
|
279
|
+
:host ::ng-deep .tk-nb, :host ::ng-deep .tk-nt, :host ::ng-deep .tk-bp{color:var(--o-tk-p)}
|
|
280
|
+
:host ::ng-deep .tk-gi{color:var(--o-add)} :host ::ng-deep .tk-gd{color:var(--o-bad-tx)}
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* What every page under /code/:repo shares: the project, the repository named in the URL (loaded
|
|
285
|
+
* once per name), the person's people and permissions, and an error line. Pages extend it and load
|
|
286
|
+
* their own data in onRepo().
|
|
287
|
+
*/
|
|
288
|
+
class RepoPage {
|
|
289
|
+
constructor() {
|
|
290
|
+
this.host = inject(CodeHost);
|
|
291
|
+
this.api = inject(CodeApi);
|
|
292
|
+
this.route = inject(ActivatedRoute);
|
|
293
|
+
this.current = inject(CurrentRepo);
|
|
294
|
+
this.name = signal('', ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
295
|
+
this.repo = signal(null, ...(ngDevMode ? [{ debugName: "repo" }] : /* istanbul ignore next */ []));
|
|
296
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
297
|
+
this.note = signal('', ...(ngDevMode ? [{ debugName: "note" }] : /* istanbul ignore next */ []));
|
|
298
|
+
this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
|
|
299
|
+
this.canWrite = computed(() => !this.repo()?.archivedAt && (this.repo()?.capabilities ?? []).includes('code.write'), ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
300
|
+
this.canAdmin = computed(() => (this.repo()?.capabilities ?? []).includes('resources.manage'), ...(ngDevMode ? [{ debugName: "canAdmin" }] : /* istanbul ignore next */ []));
|
|
301
|
+
this.canProtect = computed(() => (this.repo()?.capabilities ?? []).includes('protection.manage'), ...(ngDevMode ? [{ debugName: "canProtect" }] : /* istanbul ignore next */ []));
|
|
302
|
+
this.route.paramMap.subscribe((m) => this.name.set(m.get('repo') ?? ''));
|
|
303
|
+
effect(() => {
|
|
304
|
+
const name = this.name();
|
|
305
|
+
const p = this.host.project();
|
|
306
|
+
if (name && p)
|
|
307
|
+
untracked(() => this.loadRepo(p.id, name));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
projectId() {
|
|
311
|
+
return this.host.project()?.id ?? '';
|
|
312
|
+
}
|
|
313
|
+
link(path = '') {
|
|
314
|
+
return this.host.link(`code/${this.name()}${path ? '/' + path : ''}`);
|
|
315
|
+
}
|
|
316
|
+
reloadRepo() {
|
|
317
|
+
const p = this.host.project();
|
|
318
|
+
if (p && this.name())
|
|
319
|
+
this.loadRepo(p.id, this.name());
|
|
320
|
+
}
|
|
321
|
+
loadRepo(project, name) {
|
|
322
|
+
this.error.set('');
|
|
323
|
+
this.api.repo(project, name).subscribe({
|
|
324
|
+
next: (r) => {
|
|
325
|
+
this.repo.set(r);
|
|
326
|
+
this.current.set(project, r.name);
|
|
327
|
+
this.onRepo(r);
|
|
328
|
+
},
|
|
329
|
+
error: (e) => this.error.set(e?.status === 404 ? `There is no repository ${name} you can see in this project.` : apiMessage(e, `Could not open ${name}.`)),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoPage, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
333
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: RepoPage, isStandalone: true, ngImport: i0 }); }
|
|
334
|
+
}
|
|
335
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoPage, decorators: [{
|
|
336
|
+
type: Directive
|
|
337
|
+
}], ctorParameters: () => [] });
|
|
338
|
+
// The branch and tag picker (prototype brOpen): a filterable menu of branches, then tags.
|
|
339
|
+
class RefPicker {
|
|
340
|
+
constructor() {
|
|
341
|
+
this.api = inject(CodeApi);
|
|
342
|
+
this.host = inject(CodeHost);
|
|
343
|
+
this.el = inject(ElementRef);
|
|
344
|
+
this.repo = input.required(...(ngDevMode ? [{ debugName: "repo" }] : /* istanbul ignore next */ []));
|
|
345
|
+
this.current = input.required(...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
346
|
+
this.picked = output();
|
|
347
|
+
this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
348
|
+
this.q = signal('', ...(ngDevMode ? [{ debugName: "q" }] : /* istanbul ignore next */ []));
|
|
349
|
+
this.refs = signal([], ...(ngDevMode ? [{ debugName: "refs" }] : /* istanbul ignore next */ []));
|
|
350
|
+
this.kindOf = computed(() => (this.refs().some((r) => r.kind === 'tag' && r.name === this.current()) ? 'tag' : 'branch'), ...(ngDevMode ? [{ debugName: "kindOf" }] : /* istanbul ignore next */ []));
|
|
351
|
+
}
|
|
352
|
+
shown(kind) {
|
|
353
|
+
const q = this.q().toLowerCase();
|
|
354
|
+
return this.refs().filter((r) => r.kind === kind && (!q || r.name.toLowerCase().includes(q))).slice(0, 50);
|
|
355
|
+
}
|
|
356
|
+
toggle() {
|
|
357
|
+
this.open.set(!this.open());
|
|
358
|
+
const p = this.host.project();
|
|
359
|
+
if (this.open() && p) {
|
|
360
|
+
this.api.refs(p.id, this.repo(), 'branch').subscribe({ next: (b) => this.refs.set([...b, ...this.refs().filter((r) => r.kind === 'tag')]), error: () => undefined });
|
|
361
|
+
this.api.refs(p.id, this.repo(), 'tag').subscribe({ next: (t) => this.refs.set([...this.refs().filter((r) => r.kind === 'branch'), ...t]), error: () => undefined });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
pick(name) {
|
|
365
|
+
this.open.set(false);
|
|
366
|
+
this.picked.emit(name);
|
|
367
|
+
}
|
|
368
|
+
outside(e) {
|
|
369
|
+
if (this.open() && !this.el.nativeElement.contains(e.target))
|
|
370
|
+
this.open.set(false);
|
|
371
|
+
}
|
|
372
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RefPicker, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
373
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: RefPicker, isStandalone: true, selector: "o-ref-picker", inputs: { repo: { classPropertyName: "repo", publicName: "repo", isSignal: true, isRequired: true, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { picked: "picked" }, host: { listeners: { "document:click": "outside($event)" } }, ngImport: i0, template: `
|
|
374
|
+
<button type="button" class="o-btn o-sm" (click)="toggle()" [attr.aria-expanded]="open()" aria-haspopup="menu" data-testid="ref-picker">
|
|
375
|
+
<span class="o-faint">{{ kindOf() }}</span><b class="o-mono">{{ current() }}</b><span aria-hidden="true">⌄</span>
|
|
376
|
+
</button>
|
|
377
|
+
@if (open()) {
|
|
378
|
+
<div class="o-menu" role="menu" style="left:0;top:34px;width:280px;max-height:360px;overflow:auto">
|
|
379
|
+
<input class="o-in o-sm" placeholder="Find a branch or tag" aria-label="Find a branch or tag" style="margin:4px 0 6px" [ngModel]="q()" (ngModelChange)="q.set($event)" />
|
|
380
|
+
<div class="o-lbl" style="padding:4px 10px">Branches</div>
|
|
381
|
+
@for (b of shown('branch'); track b.name) {
|
|
382
|
+
<button type="button" role="menuitem" (click)="pick(b.name)"><span class="o-mono o-sm o-ell">{{ b.name }}</span>@if (b.name === current()) {<span class="o-k o-grn-t" aria-label="current">✓</span>}</button>
|
|
383
|
+
} @empty {<span class="o-faint o-sm" style="padding:4px 10px">No branches match.</span>}
|
|
384
|
+
@if (shown('tag').length) {
|
|
385
|
+
<div class="o-lbl" style="padding:4px 10px">Tags</div>
|
|
386
|
+
@for (t of shown('tag'); track t.name) {<button type="button" role="menuitem" (click)="pick(t.name)"><span class="o-mono o-sm o-ell">{{ t.name }}</span></button>}
|
|
387
|
+
}
|
|
388
|
+
</div>
|
|
389
|
+
}
|
|
390
|
+
`, isInline: true, styles: [":host{position:relative;display:inline-block}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
391
|
+
}
|
|
392
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RefPicker, decorators: [{
|
|
393
|
+
type: Component,
|
|
394
|
+
args: [{ selector: 'o-ref-picker', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
395
|
+
<button type="button" class="o-btn o-sm" (click)="toggle()" [attr.aria-expanded]="open()" aria-haspopup="menu" data-testid="ref-picker">
|
|
396
|
+
<span class="o-faint">{{ kindOf() }}</span><b class="o-mono">{{ current() }}</b><span aria-hidden="true">⌄</span>
|
|
397
|
+
</button>
|
|
398
|
+
@if (open()) {
|
|
399
|
+
<div class="o-menu" role="menu" style="left:0;top:34px;width:280px;max-height:360px;overflow:auto">
|
|
400
|
+
<input class="o-in o-sm" placeholder="Find a branch or tag" aria-label="Find a branch or tag" style="margin:4px 0 6px" [ngModel]="q()" (ngModelChange)="q.set($event)" />
|
|
401
|
+
<div class="o-lbl" style="padding:4px 10px">Branches</div>
|
|
402
|
+
@for (b of shown('branch'); track b.name) {
|
|
403
|
+
<button type="button" role="menuitem" (click)="pick(b.name)"><span class="o-mono o-sm o-ell">{{ b.name }}</span>@if (b.name === current()) {<span class="o-k o-grn-t" aria-label="current">✓</span>}</button>
|
|
404
|
+
} @empty {<span class="o-faint o-sm" style="padding:4px 10px">No branches match.</span>}
|
|
405
|
+
@if (shown('tag').length) {
|
|
406
|
+
<div class="o-lbl" style="padding:4px 10px">Tags</div>
|
|
407
|
+
@for (t of shown('tag'); track t.name) {<button type="button" role="menuitem" (click)="pick(t.name)"><span class="o-mono o-sm o-ell">{{ t.name }}</span></button>}
|
|
408
|
+
}
|
|
409
|
+
</div>
|
|
410
|
+
}
|
|
411
|
+
`, styles: [":host{position:relative;display:inline-block}\n"] }]
|
|
412
|
+
}], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: true }] }], picked: [{ type: i0.Output, args: ["picked"] }], outside: [{
|
|
413
|
+
type: HostListener,
|
|
414
|
+
args: ['document:click', ['$event']]
|
|
415
|
+
}] } });
|
|
416
|
+
|
|
417
|
+
// One file of a diff (prototype prDiffs): its header with +/−, the hunks highlighted, and, on a pull
|
|
418
|
+
// request, the review threads on its lines and a comment form on any line number.
|
|
419
|
+
class DiffView {
|
|
420
|
+
constructor() {
|
|
421
|
+
this.api = inject(CodeApi);
|
|
422
|
+
this.host = inject(CodeHost);
|
|
423
|
+
this.repo = input.required(...(ngDevMode ? [{ debugName: "repo" }] : /* istanbul ignore next */ []));
|
|
424
|
+
this.file = input.required(...(ngDevMode ? [{ debugName: "file" }] : /* istanbul ignore next */ []));
|
|
425
|
+
this.head = input.required(...(ngDevMode ? [{ debugName: "head" }] : /* istanbul ignore next */ []));
|
|
426
|
+
this.base = input('', ...(ngDevMode ? [{ debugName: "base" }] : /* istanbul ignore next */ []));
|
|
427
|
+
this.threads = input([], ...(ngDevMode ? [{ debugName: "threads" }] : /* istanbul ignore next */ []));
|
|
428
|
+
this.canComment = input(false, ...(ngDevMode ? [{ debugName: "canComment" }] : /* istanbul ignore next */ []));
|
|
429
|
+
this.comment = output();
|
|
430
|
+
this.reply = output();
|
|
431
|
+
this.resolve = output();
|
|
432
|
+
this.open = signal(true, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
433
|
+
this.diff = signal(null, ...(ngDevMode ? [{ debugName: "diff" }] : /* istanbul ignore next */ []));
|
|
434
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
435
|
+
this.drafting = signal('', ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
|
|
436
|
+
this.draftBody = signal('', ...(ngDevMode ? [{ debugName: "draftBody" }] : /* istanbul ignore next */ []));
|
|
437
|
+
this.replies = signal({}, ...(ngDevMode ? [{ debugName: "replies" }] : /* istanbul ignore next */ []));
|
|
438
|
+
this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
|
|
439
|
+
this.threadsHere = computed(() => this.threads().filter((t) => t.path === this.file().path), ...(ngDevMode ? [{ debugName: "threadsHere" }] : /* istanbul ignore next */ []));
|
|
440
|
+
this.ago = (s) => ago(s);
|
|
441
|
+
effect(() => {
|
|
442
|
+
const head = this.head();
|
|
443
|
+
const base = this.base();
|
|
444
|
+
const f = this.file();
|
|
445
|
+
const p = this.host.project();
|
|
446
|
+
if (p && head && !f.binary)
|
|
447
|
+
untracked(() => this.api.diff(p.id, this.repo(), head, f.path, base).subscribe({
|
|
448
|
+
next: (d) => this.diff.set(d),
|
|
449
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not load this diff.')),
|
|
450
|
+
}));
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
key(l) {
|
|
454
|
+
return `${l.kind === 'delete' ? 'old' : 'new'}:${this.lineOf(l)}`;
|
|
455
|
+
}
|
|
456
|
+
lineOf(l) {
|
|
457
|
+
return (l.kind === 'delete' ? l.old : l.new) ?? 0;
|
|
458
|
+
}
|
|
459
|
+
threadsAt(l) {
|
|
460
|
+
const side = l.kind === 'delete' ? 'old' : 'new';
|
|
461
|
+
const n = this.lineOf(l);
|
|
462
|
+
return this.threadsHere().filter((t) => !t.outdated && t.side === side && t.line === n);
|
|
463
|
+
}
|
|
464
|
+
startComment(l) {
|
|
465
|
+
if (!this.canComment())
|
|
466
|
+
return;
|
|
467
|
+
this.drafting.set(this.key(l));
|
|
468
|
+
this.draftBody.set('');
|
|
469
|
+
}
|
|
470
|
+
send(l) {
|
|
471
|
+
const body = this.draftBody().trim();
|
|
472
|
+
if (!body)
|
|
473
|
+
return;
|
|
474
|
+
this.comment.emit({ path: this.file().path, side: l.kind === 'delete' ? 'old' : 'new', line: this.lineOf(l), body });
|
|
475
|
+
this.drafting.set('');
|
|
476
|
+
this.draftBody.set('');
|
|
477
|
+
}
|
|
478
|
+
setReply(id, v) {
|
|
479
|
+
this.replies.set({ ...this.replies(), [id]: v });
|
|
480
|
+
}
|
|
481
|
+
sendReply(id) {
|
|
482
|
+
const body = (this.replies()[id] ?? '').trim();
|
|
483
|
+
if (!body)
|
|
484
|
+
return;
|
|
485
|
+
this.reply.emit({ threadId: id, body });
|
|
486
|
+
this.setReply(id, '');
|
|
487
|
+
}
|
|
488
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DiffView, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
489
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: DiffView, isStandalone: true, selector: "o-diff", inputs: { repo: { classPropertyName: "repo", publicName: "repo", isSignal: true, isRequired: true, transformFunction: null }, file: { classPropertyName: "file", publicName: "file", isSignal: true, isRequired: true, transformFunction: null }, head: { classPropertyName: "head", publicName: "head", isSignal: true, isRequired: true, transformFunction: null }, base: { classPropertyName: "base", publicName: "base", isSignal: true, isRequired: false, transformFunction: null }, threads: { classPropertyName: "threads", publicName: "threads", isSignal: true, isRequired: false, transformFunction: null }, canComment: { classPropertyName: "canComment", publicName: "canComment", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { comment: "comment", reply: "reply", resolve: "resolve" }, ngImport: i0, template: `
|
|
490
|
+
<div class="o-card" [attr.data-testid]="'diff-' + file().path">
|
|
491
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
492
|
+
<button type="button" class="o-btn o-ghost o-xs" (click)="open.set(!open())" [attr.aria-expanded]="open()" [attr.aria-label]="(open() ? 'Collapse ' : 'Expand ') + file().path">{{ open() ? '▾' : '▸' }}</button>
|
|
493
|
+
<span class="o-mono o-sm o-ell">{{ file().oldPath && file().oldPath !== file().path ? file().oldPath + ' → ' : '' }}{{ file().path }}</span>
|
|
494
|
+
<span class="o-grn-t o-sm">+{{ file().additions }}</span><span class="o-red-t o-sm">−{{ file().deletions }}</span>
|
|
495
|
+
@if (threadsHere().length) {<span class="o-faint o-sm">💬 {{ threadsHere().length }}</span>}
|
|
496
|
+
<span class="o-r"><ng-content /></span>
|
|
497
|
+
</div>
|
|
498
|
+
@if (open()) {
|
|
499
|
+
@if (file().binary) {
|
|
500
|
+
<div class="o-empty o-sm">A binary file.</div>
|
|
501
|
+
} @else if (error()) {
|
|
502
|
+
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
503
|
+
} @else if (diff(); as d) {
|
|
504
|
+
@if (d.truncated) {<div class="o-note">This diff is too large to show in full.</div>}
|
|
505
|
+
<div class="o-code" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path">
|
|
506
|
+
@for (h of d.hunks; track $index) {
|
|
507
|
+
<div class="hunk o-mono" role="row">{{ h.header }}</div>
|
|
508
|
+
@for (l of h.lines; track $index) {
|
|
509
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [class.o-del]="l.kind === 'delete'">
|
|
510
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.kind === 'delete' ? l.old : l.new }}</span>
|
|
511
|
+
<span class="o-s" aria-hidden="true">{{ l.kind === 'add' ? '+' : l.kind === 'delete' ? '−' : ' ' }}</span>
|
|
512
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
513
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
514
|
+
</div>
|
|
515
|
+
@for (t of threadsAt(l); track t.id) {
|
|
516
|
+
<div class="o-thread" [attr.data-testid]="'thread-' + t.id">
|
|
517
|
+
<div class="o-cb o-col" style="gap:6px;padding:10px 12px">
|
|
518
|
+
@for (c of t.comments; track c.id) {
|
|
519
|
+
<span class="o-sm"><b>{{ people().name(c.authorId) }}</b><span class="o-faint"> · {{ ago(c.createdAt) }}{{ c.pending ? ' · pending' : '' }}</span></span>
|
|
520
|
+
<span style="white-space:pre-wrap">{{ c.body }}</span>
|
|
521
|
+
}
|
|
522
|
+
</div>
|
|
523
|
+
@if (canComment()) {
|
|
524
|
+
<div class="o-cf">
|
|
525
|
+
<input class="o-in o-sm" placeholder="Reply…" aria-label="Reply" [ngModel]="replies()[t.id] ?? ''" (ngModelChange)="setReply(t.id, $event)" (keydown.enter)="sendReply(t.id)" />
|
|
526
|
+
<button type="button" class="o-btn o-sm" (click)="sendReply(t.id)">Reply</button>
|
|
527
|
+
<button type="button" class="o-btn o-sm" (click)="resolve.emit({ threadId: t.id, resolved: !t.resolved })">{{ t.resolved ? 'Unresolve' : 'Resolve' }}</button>
|
|
528
|
+
</div>
|
|
529
|
+
}
|
|
530
|
+
</div>
|
|
531
|
+
}
|
|
532
|
+
@if (drafting() === key(l)) {
|
|
533
|
+
<div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
|
|
534
|
+
<textarea class="o-in" rows="2" [placeholder]="'Leave a comment on line ' + lineOf(l)" [attr.aria-label]="'Comment on line ' + lineOf(l)" [ngModel]="draftBody()" (ngModelChange)="draftBody.set($event)" data-testid="line-comment"></textarea>
|
|
535
|
+
<div class="o-flex"><span class="o-faint o-xs">Markdown works.</span><button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button><button type="button" class="o-btn o-sm o-dark" [disabled]="!draftBody().trim()" (click)="send(l)" data-testid="line-comment-add">Comment</button></div>
|
|
536
|
+
</div></div>
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} @empty {
|
|
540
|
+
<div class="o-empty o-sm">No changes to show.</div>
|
|
541
|
+
}
|
|
542
|
+
</div>
|
|
543
|
+
} @else {
|
|
544
|
+
<div class="o-empty o-sm">Loading…</div>
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
</div>
|
|
548
|
+
`, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
549
|
+
}
|
|
550
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DiffView, decorators: [{
|
|
551
|
+
type: Component,
|
|
552
|
+
args: [{ selector: 'o-diff', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
553
|
+
<div class="o-card" [attr.data-testid]="'diff-' + file().path">
|
|
554
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
555
|
+
<button type="button" class="o-btn o-ghost o-xs" (click)="open.set(!open())" [attr.aria-expanded]="open()" [attr.aria-label]="(open() ? 'Collapse ' : 'Expand ') + file().path">{{ open() ? '▾' : '▸' }}</button>
|
|
556
|
+
<span class="o-mono o-sm o-ell">{{ file().oldPath && file().oldPath !== file().path ? file().oldPath + ' → ' : '' }}{{ file().path }}</span>
|
|
557
|
+
<span class="o-grn-t o-sm">+{{ file().additions }}</span><span class="o-red-t o-sm">−{{ file().deletions }}</span>
|
|
558
|
+
@if (threadsHere().length) {<span class="o-faint o-sm">💬 {{ threadsHere().length }}</span>}
|
|
559
|
+
<span class="o-r"><ng-content /></span>
|
|
560
|
+
</div>
|
|
561
|
+
@if (open()) {
|
|
562
|
+
@if (file().binary) {
|
|
563
|
+
<div class="o-empty o-sm">A binary file.</div>
|
|
564
|
+
} @else if (error()) {
|
|
565
|
+
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
566
|
+
} @else if (diff(); as d) {
|
|
567
|
+
@if (d.truncated) {<div class="o-note">This diff is too large to show in full.</div>}
|
|
568
|
+
<div class="o-code" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path">
|
|
569
|
+
@for (h of d.hunks; track $index) {
|
|
570
|
+
<div class="hunk o-mono" role="row">{{ h.header }}</div>
|
|
571
|
+
@for (l of h.lines; track $index) {
|
|
572
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [class.o-del]="l.kind === 'delete'">
|
|
573
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.kind === 'delete' ? l.old : l.new }}</span>
|
|
574
|
+
<span class="o-s" aria-hidden="true">{{ l.kind === 'add' ? '+' : l.kind === 'delete' ? '−' : ' ' }}</span>
|
|
575
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
576
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
577
|
+
</div>
|
|
578
|
+
@for (t of threadsAt(l); track t.id) {
|
|
579
|
+
<div class="o-thread" [attr.data-testid]="'thread-' + t.id">
|
|
580
|
+
<div class="o-cb o-col" style="gap:6px;padding:10px 12px">
|
|
581
|
+
@for (c of t.comments; track c.id) {
|
|
582
|
+
<span class="o-sm"><b>{{ people().name(c.authorId) }}</b><span class="o-faint"> · {{ ago(c.createdAt) }}{{ c.pending ? ' · pending' : '' }}</span></span>
|
|
583
|
+
<span style="white-space:pre-wrap">{{ c.body }}</span>
|
|
584
|
+
}
|
|
585
|
+
</div>
|
|
586
|
+
@if (canComment()) {
|
|
587
|
+
<div class="o-cf">
|
|
588
|
+
<input class="o-in o-sm" placeholder="Reply…" aria-label="Reply" [ngModel]="replies()[t.id] ?? ''" (ngModelChange)="setReply(t.id, $event)" (keydown.enter)="sendReply(t.id)" />
|
|
589
|
+
<button type="button" class="o-btn o-sm" (click)="sendReply(t.id)">Reply</button>
|
|
590
|
+
<button type="button" class="o-btn o-sm" (click)="resolve.emit({ threadId: t.id, resolved: !t.resolved })">{{ t.resolved ? 'Unresolve' : 'Resolve' }}</button>
|
|
591
|
+
</div>
|
|
592
|
+
}
|
|
593
|
+
</div>
|
|
594
|
+
}
|
|
595
|
+
@if (drafting() === key(l)) {
|
|
596
|
+
<div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
|
|
597
|
+
<textarea class="o-in" rows="2" [placeholder]="'Leave a comment on line ' + lineOf(l)" [attr.aria-label]="'Comment on line ' + lineOf(l)" [ngModel]="draftBody()" (ngModelChange)="draftBody.set($event)" data-testid="line-comment"></textarea>
|
|
598
|
+
<div class="o-flex"><span class="o-faint o-xs">Markdown works.</span><button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button><button type="button" class="o-btn o-sm o-dark" [disabled]="!draftBody().trim()" (click)="send(l)" data-testid="line-comment-add">Comment</button></div>
|
|
599
|
+
</div></div>
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} @empty {
|
|
603
|
+
<div class="o-empty o-sm">No changes to show.</div>
|
|
604
|
+
}
|
|
605
|
+
</div>
|
|
606
|
+
} @else {
|
|
607
|
+
<div class="o-empty o-sm">Loading…</div>
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
</div>
|
|
611
|
+
`, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{cursor:default;width:44px}.o-ln .o-n.cmt{cursor:pointer}.o-ln .o-n.cmt:hover{color:var(--o-acc-tx)}.hunk{color:var(--o-dim);background:var(--o-well);padding:2px 12px;font-size:11px}\n"] }]
|
|
612
|
+
}], ctorParameters: () => [], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], file: [{ type: i0.Input, args: [{ isSignal: true, alias: "file", required: true }] }], head: [{ type: i0.Input, args: [{ isSignal: true, alias: "head", required: true }] }], base: [{ type: i0.Input, args: [{ isSignal: true, alias: "base", required: false }] }], threads: [{ type: i0.Input, args: [{ isSignal: true, alias: "threads", required: false }] }], canComment: [{ type: i0.Input, args: [{ isSignal: true, alias: "canComment", required: false }] }], comment: [{ type: i0.Output, args: ["comment"] }], reply: [{ type: i0.Output, args: ["reply"] }], resolve: [{ type: i0.Output, args: ["resolve"] }] } });
|
|
613
|
+
|
|
614
|
+
// /{org}/{project}/code (prototype "code_repos"): the project's repositories.
|
|
615
|
+
class ReposPage {
|
|
616
|
+
constructor() {
|
|
617
|
+
this.host = inject(CodeHost);
|
|
618
|
+
this.api = inject(CodeApi);
|
|
619
|
+
this.repos = signal([], ...(ngDevMode ? [{ debugName: "repos" }] : /* istanbul ignore next */ []));
|
|
620
|
+
this.loaded = signal(false, ...(ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []));
|
|
621
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
622
|
+
this.ago = (s) => ago(s);
|
|
623
|
+
this.canWrite = computed(() => {
|
|
624
|
+
const p = this.host.project();
|
|
625
|
+
return !!p && !p.archivedAt && p.capabilities.includes('code.write');
|
|
626
|
+
}, ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
627
|
+
effect(() => {
|
|
628
|
+
const p = this.host.project();
|
|
629
|
+
if (p)
|
|
630
|
+
untracked(() => this.api.repos(p.id).subscribe({
|
|
631
|
+
next: (r) => { this.repos.set(r); this.loaded.set(true); },
|
|
632
|
+
error: (e) => { this.loaded.set(true); this.error.set(apiMessage(e, 'Could not load the repositories.')); },
|
|
633
|
+
}));
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
sizeOf(b) {
|
|
637
|
+
return b < 1024 * 1024 ? `${Math.max(1, Math.round(b / 1024))} KB` : `${(b / 1024 / 1024).toFixed(1)} MB`;
|
|
638
|
+
}
|
|
639
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ReposPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
640
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ReposPage, isStandalone: true, selector: "o-code-repos", ngImport: i0, template: `
|
|
641
|
+
<div class="o-page">
|
|
642
|
+
<div class="o-ph">
|
|
643
|
+
<div class="o-col"><span class="o-eyebrow">{{ host.project()?.key }} · {{ repos().length }} {{ repos().length === 1 ? 'repository' : 'repositories' }}</span><h1 class="o-t">Repositories</h1></div>
|
|
644
|
+
@if (canWrite()) {
|
|
645
|
+
<div class="o-acts">
|
|
646
|
+
<a class="o-btn" [routerLink]="host.link('code/new')" [queryParams]="{ import: 1 }">Import repository</a>
|
|
647
|
+
<a class="o-btn o-dark" [routerLink]="host.link('code/new')" data-testid="new-repo">+ New repository</a>
|
|
648
|
+
</div>
|
|
649
|
+
}
|
|
650
|
+
</div>
|
|
651
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
652
|
+
<div class="o-card" style="overflow-x:auto">
|
|
653
|
+
<div class="o-tbl" role="table" aria-label="Repositories" style="min-width:640px">
|
|
654
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Name</div><div class="o-th" role="columnheader">Default branch</div><div class="o-th" role="columnheader">Last push</div><div class="o-th" role="columnheader">Size</div><div class="o-th" role="columnheader">Visibility</div></div></div>
|
|
655
|
+
<div class="o-tbody" role="rowgroup">
|
|
656
|
+
@for (r of repos(); track r.name) {
|
|
657
|
+
<a class="o-tr o-click" role="row" [routerLink]="host.link('code/' + r.name)" [attr.data-testid]="'repo-' + r.name">
|
|
658
|
+
<div class="o-td" role="cell"><div class="o-col" style="gap:2px"><b class="o-mono o-b5">{{ r.name }}</b>@if (r.description) {<span class="o-faint o-sm">{{ r.description }}</span>}</div></div>
|
|
659
|
+
<div class="o-td o-mono o-sm" role="cell">{{ r.defaultBranch }}</div>
|
|
660
|
+
<div class="o-td o-mut" role="cell">{{ r.importState === 'running' || r.importState === 'queued' ? 'Importing…' : r.empty ? 'Empty' : ago(r.pushedAt) }}</div>
|
|
661
|
+
<div class="o-td o-mut o-sm" role="cell">{{ sizeOf(r.sizeBytes) }}</div>
|
|
662
|
+
<div class="o-td" role="cell"><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb" style="margin-left:4px">Archived</span>}</div>
|
|
663
|
+
</a>
|
|
664
|
+
} @empty {
|
|
665
|
+
<div class="o-empty">{{ loaded() ? 'No repositories yet. Create one, or import from GitHub, GitLab, Bitbucket or any Git server.' : 'Loading…' }}</div>
|
|
666
|
+
}
|
|
667
|
+
</div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
672
|
+
}
|
|
673
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ReposPage, decorators: [{
|
|
674
|
+
type: Component,
|
|
675
|
+
args: [{
|
|
676
|
+
selector: 'o-code-repos',
|
|
677
|
+
imports: [RouterLink],
|
|
678
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
679
|
+
template: `
|
|
680
|
+
<div class="o-page">
|
|
681
|
+
<div class="o-ph">
|
|
682
|
+
<div class="o-col"><span class="o-eyebrow">{{ host.project()?.key }} · {{ repos().length }} {{ repos().length === 1 ? 'repository' : 'repositories' }}</span><h1 class="o-t">Repositories</h1></div>
|
|
683
|
+
@if (canWrite()) {
|
|
684
|
+
<div class="o-acts">
|
|
685
|
+
<a class="o-btn" [routerLink]="host.link('code/new')" [queryParams]="{ import: 1 }">Import repository</a>
|
|
686
|
+
<a class="o-btn o-dark" [routerLink]="host.link('code/new')" data-testid="new-repo">+ New repository</a>
|
|
687
|
+
</div>
|
|
688
|
+
}
|
|
689
|
+
</div>
|
|
690
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
691
|
+
<div class="o-card" style="overflow-x:auto">
|
|
692
|
+
<div class="o-tbl" role="table" aria-label="Repositories" style="min-width:640px">
|
|
693
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Name</div><div class="o-th" role="columnheader">Default branch</div><div class="o-th" role="columnheader">Last push</div><div class="o-th" role="columnheader">Size</div><div class="o-th" role="columnheader">Visibility</div></div></div>
|
|
694
|
+
<div class="o-tbody" role="rowgroup">
|
|
695
|
+
@for (r of repos(); track r.name) {
|
|
696
|
+
<a class="o-tr o-click" role="row" [routerLink]="host.link('code/' + r.name)" [attr.data-testid]="'repo-' + r.name">
|
|
697
|
+
<div class="o-td" role="cell"><div class="o-col" style="gap:2px"><b class="o-mono o-b5">{{ r.name }}</b>@if (r.description) {<span class="o-faint o-sm">{{ r.description }}</span>}</div></div>
|
|
698
|
+
<div class="o-td o-mono o-sm" role="cell">{{ r.defaultBranch }}</div>
|
|
699
|
+
<div class="o-td o-mut" role="cell">{{ r.importState === 'running' || r.importState === 'queued' ? 'Importing…' : r.empty ? 'Empty' : ago(r.pushedAt) }}</div>
|
|
700
|
+
<div class="o-td o-mut o-sm" role="cell">{{ sizeOf(r.sizeBytes) }}</div>
|
|
701
|
+
<div class="o-td" role="cell"><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb" style="margin-left:4px">Archived</span>}</div>
|
|
702
|
+
</a>
|
|
703
|
+
} @empty {
|
|
704
|
+
<div class="o-empty">{{ loaded() ? 'No repositories yet. Create one, or import from GitHub, GitLab, Bitbucket or any Git server.' : 'Loading…' }}</div>
|
|
705
|
+
}
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
`,
|
|
711
|
+
}]
|
|
712
|
+
}], ctorParameters: () => [] });
|
|
713
|
+
// /{org}/{project}/code/new (prototype "code_new"): create an empty repository with optional
|
|
714
|
+
// README, .gitignore and licence, or import one from a URL; then the quick-setup commands.
|
|
715
|
+
class NewRepoPage {
|
|
716
|
+
constructor() {
|
|
717
|
+
this.host = inject(CodeHost);
|
|
718
|
+
this.api = inject(CodeApi);
|
|
719
|
+
this.route = inject(ActivatedRoute);
|
|
720
|
+
this.mode = signal('create', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
721
|
+
this.made = signal(null, ...(ngDevMode ? [{ debugName: "made" }] : /* istanbul ignore next */ []));
|
|
722
|
+
this.proto = signal('https', ...(ngDevMode ? [{ debugName: "proto" }] : /* istanbul ignore next */ []));
|
|
723
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
724
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
725
|
+
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
726
|
+
this.url = computed(() => (this.proto() === 'ssh' ? this.made()?.cloneSsh : this.made()?.cloneHttps) || '', ...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
|
|
727
|
+
this.repoName = '';
|
|
728
|
+
this.desc = '';
|
|
729
|
+
this.vis = 'private';
|
|
730
|
+
this.branch = 'main';
|
|
731
|
+
this.readme = true;
|
|
732
|
+
this.gitignore = '';
|
|
733
|
+
this.licence = '';
|
|
734
|
+
this.url_ = '';
|
|
735
|
+
this.user = '';
|
|
736
|
+
this.token = '';
|
|
737
|
+
this.route.queryParamMap.subscribe((q) => this.mode.set(q.get('import') ? 'import' : 'create'));
|
|
738
|
+
}
|
|
739
|
+
nameFromUrl() {
|
|
740
|
+
if (this.repoName.trim())
|
|
741
|
+
return;
|
|
742
|
+
const m = this.url_.trim().match(/([^/:]+?)(\.git)?\/?$/);
|
|
743
|
+
if (m)
|
|
744
|
+
this.repoName = m[1];
|
|
745
|
+
}
|
|
746
|
+
copyUrl() {
|
|
747
|
+
void copy(this.url()).then(() => { this.copied.set(true); setTimeout(() => this.copied.set(false), 1500); });
|
|
748
|
+
}
|
|
749
|
+
create() {
|
|
750
|
+
const p = this.host.project();
|
|
751
|
+
if (!p)
|
|
752
|
+
return;
|
|
753
|
+
this.busy.set(true);
|
|
754
|
+
this.error.set('');
|
|
755
|
+
this.api.create(p.id, { name: this.repoName.trim(), description: this.desc.trim(), visibility: this.vis, defaultBranch: this.branch.trim() || 'main', readme: this.readme, gitignore: this.gitignore, licence: this.licence }).subscribe({
|
|
756
|
+
next: (r) => { this.busy.set(false); this.made.set(r); },
|
|
757
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not create the repository.')); },
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
importRepo() {
|
|
761
|
+
const p = this.host.project();
|
|
762
|
+
if (!p)
|
|
763
|
+
return;
|
|
764
|
+
this.busy.set(true);
|
|
765
|
+
this.error.set('');
|
|
766
|
+
this.api.import(p.id, { name: this.repoName.trim(), url: this.url_.trim(), username: this.user.trim() || undefined, token: this.token || undefined, visibility: this.vis }).subscribe({
|
|
767
|
+
next: (r) => { this.busy.set(false); this.token = ''; this.made.set(r); },
|
|
768
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not start the import.')); },
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewRepoPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
772
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: NewRepoPage, isStandalone: true, selector: "o-code-new", ngImport: i0, template: `
|
|
773
|
+
<div class="o-page o-narrow">
|
|
774
|
+
<div class="o-ph"><div class="o-col"><span class="o-eyebrow">{{ host.project()?.key }}</span><h1 class="o-t">Add a repository</h1></div></div>
|
|
775
|
+
<div class="o-tabs" role="tablist">
|
|
776
|
+
<button type="button" role="tab" class="o-tab" [class.o-on]="mode() === 'create'" [attr.aria-selected]="mode() === 'create'" (click)="mode.set('create')">Create</button>
|
|
777
|
+
<button type="button" role="tab" class="o-tab" [class.o-on]="mode() === 'import'" [attr.aria-selected]="mode() === 'import'" (click)="mode.set('import')">Import from URL</button>
|
|
778
|
+
</div>
|
|
779
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
780
|
+
|
|
781
|
+
@if (made(); as r) {
|
|
782
|
+
<div class="o-card">
|
|
783
|
+
<div class="o-ch"><b class="o-mono">{{ r.name }}</b><span class="o-chip">{{ r.empty ? 'empty' : r.importState ? 'importing' : 'ready' }}</span><span class="o-r">Quick setup</span></div>
|
|
784
|
+
<div class="o-cb o-col" style="gap:14px">
|
|
785
|
+
<div class="o-flex">
|
|
786
|
+
<div class="o-seg" role="group" aria-label="Protocol"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
787
|
+
<div class="o-copy" style="flex:1"><span data-testid="clone-url">{{ url() }}</span><button type="button" class="o-btn o-xs" (click)="copyUrl()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
788
|
+
</div>
|
|
789
|
+
@if (r.empty) {
|
|
790
|
+
<b class="o-sm">Create a new repository on the command line</b>
|
|
791
|
+
<div class="o-term"><div>echo "# {{ r.name }}" >> README.md</div><div>git init && git add README.md</div><div>git commit -m "first commit"</div><div>git branch -M {{ r.defaultBranch }}</div><div>git remote add origin {{ url() }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
792
|
+
<b class="o-sm">…or push an existing repository</b>
|
|
793
|
+
<div class="o-term"><div>git remote add origin {{ url() }}</div><div>git branch -M {{ r.defaultBranch }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
794
|
+
}
|
|
795
|
+
<a class="o-btn o-dark" style="align-self:flex-start" [routerLink]="host.link('code/' + r.name)">Open {{ r.name }}</a>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
} @else if (mode() === 'create') {
|
|
799
|
+
<form class="o-card o-cb o-form" (ngSubmit)="create()">
|
|
800
|
+
<div class="o-fld"><label for="nr-name">Repository name</label><input id="nr-name" class="o-in o-mono" name="name" [(ngModel)]="repoName" placeholder="payments-api" autocomplete="off" data-testid="repo-name" /><span class="o-hint">Letters, digits, dots, dashes and underscores.</span></div>
|
|
801
|
+
<div class="o-fld"><label for="nr-desc">Description <span class="o-faint">(optional)</span></label><input id="nr-desc" class="o-in" name="desc" [(ngModel)]="desc" placeholder="What lives here?" /></div>
|
|
802
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
803
|
+
<div class="o-g2">
|
|
804
|
+
<button type="button" class="o-opt" [class.o-on]="vis === 'private'" (click)="vis = 'private'"><span class="o-rad" [class.o-on]="vis === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
805
|
+
<button type="button" class="o-opt" [class.o-on]="vis === 'internal'" (click)="vis = 'internal'"><span class="o-rad" [class.o-on]="vis === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
<div class="o-g4">
|
|
809
|
+
<div class="o-fld"><label for="nr-br">Default branch</label><input id="nr-br" class="o-in o-mono" name="branch" [(ngModel)]="branch" /></div>
|
|
810
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0" id="nr-readme">README</span><button type="button" class="o-tog" role="switch" aria-labelledby="nr-readme" [class.o-on]="readme" [attr.aria-checked]="readme" (click)="readme = !readme" data-testid="repo-readme"></button></div>
|
|
811
|
+
<div class="o-fld"><label for="nr-gi">.gitignore</label><select id="nr-gi" class="o-in" name="gi" [(ngModel)]="gitignore"><option value="">None</option><option value="node">Node</option><option value="go">Go</option><option value="python">Python</option></select></div>
|
|
812
|
+
<div class="o-fld"><label for="nr-lic">Licence</label><select id="nr-lic" class="o-in" name="lic" [(ngModel)]="licence"><option value="">None</option><option value="MIT">MIT</option><option value="Apache-2.0">Apache 2.0</option></select></div>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="o-flex"><span class="o-faint o-sm">Push limits: 2 GB per push, 100 MB per file.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !repoName.trim()" data-testid="repo-create">Create repository</button></div>
|
|
815
|
+
</form>
|
|
816
|
+
} @else {
|
|
817
|
+
<form class="o-card o-cb o-form" (ngSubmit)="importRepo()">
|
|
818
|
+
<div class="o-fld"><label for="ir-url">Git URL</label><input id="ir-url" class="o-in o-mono" name="url" [(ngModel)]="url_" placeholder="https://github.com/acme/payments-api.git" (blur)="nameFromUrl()" /><span class="o-hint">GitHub, GitLab, Bitbucket or any Git server over HTTPS. All branches and tags are copied.</span></div>
|
|
819
|
+
<div class="o-fld"><label for="ir-name">Repository name</label><input id="ir-name" class="o-in o-mono" name="iname" [(ngModel)]="repoName" /></div>
|
|
820
|
+
<div class="o-g2">
|
|
821
|
+
<div class="o-fld"><label for="ir-user">Username <span class="o-faint">(private repos)</span></label><input id="ir-user" class="o-in" name="user" [(ngModel)]="user" autocomplete="off" /></div>
|
|
822
|
+
<div class="o-fld"><label for="ir-tok">Access token</label><input id="ir-tok" class="o-in" type="password" name="token" [(ngModel)]="token" autocomplete="off" /></div>
|
|
823
|
+
</div>
|
|
824
|
+
<div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Keep as a pull mirror"></button><span class="o-sm">Keep as a pull mirror</span><b class="o-ph-tag o-later">Later</b></div>
|
|
825
|
+
<div class="o-flex"><span class="o-faint o-sm">Imports run in the background; you'll get an inbox message when done.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !url_.trim() || !repoName.trim()">Import</button></div>
|
|
826
|
+
</form>
|
|
827
|
+
}
|
|
828
|
+
</div>
|
|
829
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
830
|
+
}
|
|
831
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewRepoPage, decorators: [{
|
|
832
|
+
type: Component,
|
|
833
|
+
args: [{
|
|
834
|
+
selector: 'o-code-new',
|
|
835
|
+
imports: [FormsModule, RouterLink],
|
|
836
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
837
|
+
template: `
|
|
838
|
+
<div class="o-page o-narrow">
|
|
839
|
+
<div class="o-ph"><div class="o-col"><span class="o-eyebrow">{{ host.project()?.key }}</span><h1 class="o-t">Add a repository</h1></div></div>
|
|
840
|
+
<div class="o-tabs" role="tablist">
|
|
841
|
+
<button type="button" role="tab" class="o-tab" [class.o-on]="mode() === 'create'" [attr.aria-selected]="mode() === 'create'" (click)="mode.set('create')">Create</button>
|
|
842
|
+
<button type="button" role="tab" class="o-tab" [class.o-on]="mode() === 'import'" [attr.aria-selected]="mode() === 'import'" (click)="mode.set('import')">Import from URL</button>
|
|
843
|
+
</div>
|
|
844
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
845
|
+
|
|
846
|
+
@if (made(); as r) {
|
|
847
|
+
<div class="o-card">
|
|
848
|
+
<div class="o-ch"><b class="o-mono">{{ r.name }}</b><span class="o-chip">{{ r.empty ? 'empty' : r.importState ? 'importing' : 'ready' }}</span><span class="o-r">Quick setup</span></div>
|
|
849
|
+
<div class="o-cb o-col" style="gap:14px">
|
|
850
|
+
<div class="o-flex">
|
|
851
|
+
<div class="o-seg" role="group" aria-label="Protocol"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
852
|
+
<div class="o-copy" style="flex:1"><span data-testid="clone-url">{{ url() }}</span><button type="button" class="o-btn o-xs" (click)="copyUrl()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
853
|
+
</div>
|
|
854
|
+
@if (r.empty) {
|
|
855
|
+
<b class="o-sm">Create a new repository on the command line</b>
|
|
856
|
+
<div class="o-term"><div>echo "# {{ r.name }}" >> README.md</div><div>git init && git add README.md</div><div>git commit -m "first commit"</div><div>git branch -M {{ r.defaultBranch }}</div><div>git remote add origin {{ url() }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
857
|
+
<b class="o-sm">…or push an existing repository</b>
|
|
858
|
+
<div class="o-term"><div>git remote add origin {{ url() }}</div><div>git branch -M {{ r.defaultBranch }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
859
|
+
}
|
|
860
|
+
<a class="o-btn o-dark" style="align-self:flex-start" [routerLink]="host.link('code/' + r.name)">Open {{ r.name }}</a>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
} @else if (mode() === 'create') {
|
|
864
|
+
<form class="o-card o-cb o-form" (ngSubmit)="create()">
|
|
865
|
+
<div class="o-fld"><label for="nr-name">Repository name</label><input id="nr-name" class="o-in o-mono" name="name" [(ngModel)]="repoName" placeholder="payments-api" autocomplete="off" data-testid="repo-name" /><span class="o-hint">Letters, digits, dots, dashes and underscores.</span></div>
|
|
866
|
+
<div class="o-fld"><label for="nr-desc">Description <span class="o-faint">(optional)</span></label><input id="nr-desc" class="o-in" name="desc" [(ngModel)]="desc" placeholder="What lives here?" /></div>
|
|
867
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
868
|
+
<div class="o-g2">
|
|
869
|
+
<button type="button" class="o-opt" [class.o-on]="vis === 'private'" (click)="vis = 'private'"><span class="o-rad" [class.o-on]="vis === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
870
|
+
<button type="button" class="o-opt" [class.o-on]="vis === 'internal'" (click)="vis = 'internal'"><span class="o-rad" [class.o-on]="vis === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
<div class="o-g4">
|
|
874
|
+
<div class="o-fld"><label for="nr-br">Default branch</label><input id="nr-br" class="o-in o-mono" name="branch" [(ngModel)]="branch" /></div>
|
|
875
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0" id="nr-readme">README</span><button type="button" class="o-tog" role="switch" aria-labelledby="nr-readme" [class.o-on]="readme" [attr.aria-checked]="readme" (click)="readme = !readme" data-testid="repo-readme"></button></div>
|
|
876
|
+
<div class="o-fld"><label for="nr-gi">.gitignore</label><select id="nr-gi" class="o-in" name="gi" [(ngModel)]="gitignore"><option value="">None</option><option value="node">Node</option><option value="go">Go</option><option value="python">Python</option></select></div>
|
|
877
|
+
<div class="o-fld"><label for="nr-lic">Licence</label><select id="nr-lic" class="o-in" name="lic" [(ngModel)]="licence"><option value="">None</option><option value="MIT">MIT</option><option value="Apache-2.0">Apache 2.0</option></select></div>
|
|
878
|
+
</div>
|
|
879
|
+
<div class="o-flex"><span class="o-faint o-sm">Push limits: 2 GB per push, 100 MB per file.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !repoName.trim()" data-testid="repo-create">Create repository</button></div>
|
|
880
|
+
</form>
|
|
881
|
+
} @else {
|
|
882
|
+
<form class="o-card o-cb o-form" (ngSubmit)="importRepo()">
|
|
883
|
+
<div class="o-fld"><label for="ir-url">Git URL</label><input id="ir-url" class="o-in o-mono" name="url" [(ngModel)]="url_" placeholder="https://github.com/acme/payments-api.git" (blur)="nameFromUrl()" /><span class="o-hint">GitHub, GitLab, Bitbucket or any Git server over HTTPS. All branches and tags are copied.</span></div>
|
|
884
|
+
<div class="o-fld"><label for="ir-name">Repository name</label><input id="ir-name" class="o-in o-mono" name="iname" [(ngModel)]="repoName" /></div>
|
|
885
|
+
<div class="o-g2">
|
|
886
|
+
<div class="o-fld"><label for="ir-user">Username <span class="o-faint">(private repos)</span></label><input id="ir-user" class="o-in" name="user" [(ngModel)]="user" autocomplete="off" /></div>
|
|
887
|
+
<div class="o-fld"><label for="ir-tok">Access token</label><input id="ir-tok" class="o-in" type="password" name="token" [(ngModel)]="token" autocomplete="off" /></div>
|
|
888
|
+
</div>
|
|
889
|
+
<div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Keep as a pull mirror"></button><span class="o-sm">Keep as a pull mirror</span><b class="o-ph-tag o-later">Later</b></div>
|
|
890
|
+
<div class="o-flex"><span class="o-faint o-sm">Imports run in the background; you'll get an inbox message when done.</span><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !url_.trim() || !repoName.trim()">Import</button></div>
|
|
891
|
+
</form>
|
|
892
|
+
}
|
|
893
|
+
</div>
|
|
894
|
+
`,
|
|
895
|
+
}]
|
|
896
|
+
}], ctorParameters: () => [] });
|
|
897
|
+
// /{org}/{project}/code/{files,commits,branches,settings}: the project-level tabs open the repository
|
|
898
|
+
// the person was last in (else the first one) at that tab.
|
|
899
|
+
class CurrentRepoPage {
|
|
900
|
+
constructor() {
|
|
901
|
+
this.host = inject(CodeHost);
|
|
902
|
+
this.api = inject(CodeApi);
|
|
903
|
+
this.route = inject(ActivatedRoute);
|
|
904
|
+
this.router = inject(Router);
|
|
905
|
+
this.current = inject(CurrentRepo);
|
|
906
|
+
this.none = signal(false, ...(ngDevMode ? [{ debugName: "none" }] : /* istanbul ignore next */ []));
|
|
907
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
908
|
+
effect(() => {
|
|
909
|
+
const p = this.host.project();
|
|
910
|
+
if (!p)
|
|
911
|
+
return;
|
|
912
|
+
untracked(() => {
|
|
913
|
+
const tab = String(this.route.snapshot.data['codeTab'] ?? '');
|
|
914
|
+
this.api.repos(p.id).subscribe({
|
|
915
|
+
next: (rs) => {
|
|
916
|
+
if (!rs.length)
|
|
917
|
+
return this.none.set(true);
|
|
918
|
+
const last = this.current.get(p.id);
|
|
919
|
+
const repo = rs.find((r) => r.name === last)?.name ?? rs[0].name;
|
|
920
|
+
void this.router.navigate(this.host.link(`code/${repo}${tab ? '/' + tab : ''}`), { replaceUrl: true });
|
|
921
|
+
},
|
|
922
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not load the repositories.')),
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CurrentRepoPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
928
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: CurrentRepoPage, isStandalone: true, selector: "o-code-current", ngImport: i0, template: `
|
|
929
|
+
<div class="o-page o-narrow">
|
|
930
|
+
@if (none()) {
|
|
931
|
+
<div class="o-card o-empty"><p>No repositories yet.</p><a class="o-btn" style="margin-top:14px" [routerLink]="host.link('code/new')">+ New repository</a></div>
|
|
932
|
+
} @else if (error()) {
|
|
933
|
+
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
934
|
+
} @else {
|
|
935
|
+
<div class="o-card"><div class="o-empty">Opening the repository…</div></div>
|
|
936
|
+
}
|
|
937
|
+
</div>
|
|
938
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
939
|
+
}
|
|
940
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CurrentRepoPage, decorators: [{
|
|
941
|
+
type: Component,
|
|
942
|
+
args: [{
|
|
943
|
+
selector: 'o-code-current',
|
|
944
|
+
imports: [RouterLink],
|
|
945
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
946
|
+
template: `
|
|
947
|
+
<div class="o-page o-narrow">
|
|
948
|
+
@if (none()) {
|
|
949
|
+
<div class="o-card o-empty"><p>No repositories yet.</p><a class="o-btn" style="margin-top:14px" [routerLink]="host.link('code/new')">+ New repository</a></div>
|
|
950
|
+
} @else if (error()) {
|
|
951
|
+
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
952
|
+
} @else {
|
|
953
|
+
<div class="o-card"><div class="o-empty">Opening the repository…</div></div>
|
|
954
|
+
}
|
|
955
|
+
</div>
|
|
956
|
+
`,
|
|
957
|
+
}]
|
|
958
|
+
}], ctorParameters: () => [] });
|
|
959
|
+
|
|
960
|
+
// /{org}/{project}/code/{repo}?ref=&path= (prototype "code_repo"): the repository's files at a
|
|
961
|
+
// branch, tag or commit, the last commit of each entry, the README, the clone menu and languages.
|
|
962
|
+
class RepoHomePage extends RepoPage {
|
|
963
|
+
constructor() {
|
|
964
|
+
super();
|
|
965
|
+
this.router = inject(Router);
|
|
966
|
+
this.el = inject(ElementRef);
|
|
967
|
+
this.tree = signal(null, ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
|
|
968
|
+
this.langs = signal([], ...(ngDevMode ? [{ debugName: "langs" }] : /* istanbul ignore next */ []));
|
|
969
|
+
this.ref = signal('', ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
|
|
970
|
+
this.path = signal('', ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
971
|
+
this.proto = signal('https', ...(ngDevMode ? [{ debugName: "proto" }] : /* istanbul ignore next */ []));
|
|
972
|
+
this.cloneOpen = signal(false, ...(ngDevMode ? [{ debugName: "cloneOpen" }] : /* istanbul ignore next */ []));
|
|
973
|
+
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
974
|
+
this.finding = signal(false, ...(ngDevMode ? [{ debugName: "finding" }] : /* istanbul ignore next */ []));
|
|
975
|
+
this.fq = signal('', ...(ngDevMode ? [{ debugName: "fq" }] : /* istanbul ignore next */ []));
|
|
976
|
+
this.files = signal([], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
|
|
977
|
+
this.ago = (s) => ago(s);
|
|
978
|
+
this.initials = initials;
|
|
979
|
+
this.colour = langColour;
|
|
980
|
+
this.cloneUrl = computed(() => (this.proto() === 'ssh' ? this.repo()?.cloneSsh : this.repo()?.cloneHttps) || '', ...(ngDevMode ? [{ debugName: "cloneUrl" }] : /* istanbul ignore next */ []));
|
|
981
|
+
this.crumbs = computed(() => {
|
|
982
|
+
const parts = this.path().split('/').filter(Boolean);
|
|
983
|
+
return parts.map((name, i) => ({ name, path: parts.slice(0, i + 1).join('/') }));
|
|
984
|
+
}, ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
|
|
985
|
+
this.parent = computed(() => this.path().split('/').slice(0, -1).join('/'), ...(ngDevMode ? [{ debugName: "parent" }] : /* istanbul ignore next */ []));
|
|
986
|
+
this.found = computed(() => {
|
|
987
|
+
const q = this.fq().toLowerCase();
|
|
988
|
+
return this.files().filter((f) => !q || f.toLowerCase().includes(q)).slice(0, 50);
|
|
989
|
+
}, ...(ngDevMode ? [{ debugName: "found" }] : /* istanbul ignore next */ []));
|
|
990
|
+
this.langLabel = computed(() => this.langs().map((l) => `${l.name} ${l.percent.toFixed(0)}%`).join(', '), ...(ngDevMode ? [{ debugName: "langLabel" }] : /* istanbul ignore next */ []));
|
|
991
|
+
this.route.queryParamMap.subscribe((q) => {
|
|
992
|
+
this.ref.set(q.get('ref') ?? '');
|
|
993
|
+
this.path.set(q.get('path') ?? '');
|
|
994
|
+
const r = this.repo();
|
|
995
|
+
if (r && r.name === this.name())
|
|
996
|
+
this.load(r);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
onRepo(r) {
|
|
1000
|
+
this.load(r);
|
|
1001
|
+
if (!r.empty)
|
|
1002
|
+
this.api.languages(this.projectId(), r.name, this.ref() || r.defaultBranch).subscribe({ next: (l) => this.langs.set(l), error: () => undefined });
|
|
1003
|
+
}
|
|
1004
|
+
qp(path) {
|
|
1005
|
+
return { ref: this.ref() || null, path: path || null };
|
|
1006
|
+
}
|
|
1007
|
+
go(ref, path) {
|
|
1008
|
+
void this.router.navigate(this.link(), { queryParams: { ref, path: path || null } });
|
|
1009
|
+
}
|
|
1010
|
+
copyClone() {
|
|
1011
|
+
void copy(this.cloneUrl()).then(() => { this.copied.set(true); setTimeout(() => this.copied.set(false), 1500); });
|
|
1012
|
+
}
|
|
1013
|
+
archive(format) {
|
|
1014
|
+
const r = this.repo();
|
|
1015
|
+
if (!r)
|
|
1016
|
+
return;
|
|
1017
|
+
this.api.archiveUrl(this.projectId(), r.name, this.ref() || r.defaultBranch, format).subscribe({
|
|
1018
|
+
next: (u) => { if (u)
|
|
1019
|
+
location.assign(u); },
|
|
1020
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not prepare the download.')),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
openFirst() {
|
|
1024
|
+
const f = this.found()[0];
|
|
1025
|
+
if (f) {
|
|
1026
|
+
this.finding.set(false);
|
|
1027
|
+
void this.router.navigate(this.link('blob'), { queryParams: this.qp(f) });
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/** T opens Go to file (prototype). */
|
|
1031
|
+
key(e) {
|
|
1032
|
+
const t = e.target;
|
|
1033
|
+
if (e.key !== 't' || e.metaKey || e.ctrlKey || e.altKey || t?.closest('input,textarea,select,[contenteditable]'))
|
|
1034
|
+
return;
|
|
1035
|
+
if (this.repo() && !this.repo().empty && !this.finding()) {
|
|
1036
|
+
e.preventDefault();
|
|
1037
|
+
this.openFinder();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
load(r) {
|
|
1041
|
+
if (r.empty || (r.importState && r.importState !== 'done'))
|
|
1042
|
+
return;
|
|
1043
|
+
const ref = this.ref() || r.defaultBranch;
|
|
1044
|
+
this.tree.set(null);
|
|
1045
|
+
this.api.tree(this.projectId(), r.name, ref, this.path()).subscribe({
|
|
1046
|
+
next: (t) => this.tree.set(t),
|
|
1047
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not read the files.')),
|
|
1048
|
+
});
|
|
1049
|
+
this.files.set([]);
|
|
1050
|
+
}
|
|
1051
|
+
openFinder() {
|
|
1052
|
+
const r = this.repo();
|
|
1053
|
+
if (!r || r.empty)
|
|
1054
|
+
return;
|
|
1055
|
+
this.finding.set(true);
|
|
1056
|
+
this.fq.set('');
|
|
1057
|
+
if (!this.files().length)
|
|
1058
|
+
this.api.files(this.projectId(), r.name, this.ref() || r.defaultBranch).subscribe({ next: (f) => this.files.set(f.files), error: () => undefined });
|
|
1059
|
+
setTimeout(() => this.el.nativeElement.querySelector('input[aria-label="Find a file"]')?.focus());
|
|
1060
|
+
}
|
|
1061
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoHomePage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1062
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: RepoHomePage, isStandalone: true, selector: "o-code-repo", host: { listeners: { "document:keydown": "key($event)" } }, usesInheritance: true, ngImport: i0, template: `
|
|
1063
|
+
<div class="o-page">
|
|
1064
|
+
@if (repo(); as r) {
|
|
1065
|
+
<div class="o-ph">
|
|
1066
|
+
<div class="o-col">
|
|
1067
|
+
<span class="o-eyebrow">{{ r.projectKey }}</span>
|
|
1068
|
+
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}</div>
|
|
1069
|
+
@if (r.description) {<span class="o-mut">{{ r.description }}</span>}
|
|
1070
|
+
</div>
|
|
1071
|
+
@if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
|
|
1072
|
+
</div>
|
|
1073
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1074
|
+
|
|
1075
|
+
@if (r.importState && r.importState !== 'done') {
|
|
1076
|
+
<div class="o-card o-cb o-col" style="gap:6px">
|
|
1077
|
+
<b>{{ r.importState === 'failed' ? 'The import failed' : 'Importing from ' + r.importSource }}</b>
|
|
1078
|
+
<span class="o-mut o-sm">{{ r.importState === 'failed' ? r.importError : 'This runs in the background; the inbox tells you when it is done.' }}</span>
|
|
1079
|
+
</div>
|
|
1080
|
+
} @else if (r.empty) {
|
|
1081
|
+
<div class="o-card">
|
|
1082
|
+
<div class="o-ch"><b>This repository is empty</b><span class="o-r">Quick setup</span></div>
|
|
1083
|
+
<div class="o-cb o-col" style="gap:14px">
|
|
1084
|
+
<div class="o-flex">
|
|
1085
|
+
<div class="o-seg" role="group" aria-label="Protocol"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
1086
|
+
<div class="o-copy" style="flex:1"><span data-testid="clone-url">{{ cloneUrl() }}</span><button type="button" class="o-btn o-xs" (click)="copyClone()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
1087
|
+
</div>
|
|
1088
|
+
<b class="o-sm">Push an existing repository</b>
|
|
1089
|
+
<div class="o-term"><div>git remote add origin {{ cloneUrl() }}</div><div>git branch -M {{ r.defaultBranch }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
} @else {
|
|
1093
|
+
<div class="o-flex o-wrap">
|
|
1094
|
+
<o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="go($event, '')" />
|
|
1095
|
+
<nav class="o-flex o-sm o-mono" aria-label="Path">
|
|
1096
|
+
<a [routerLink]="link()" [queryParams]="qp('')">{{ r.name }}</a>
|
|
1097
|
+
@for (c of crumbs(); track c.path) {<span class="o-faint">/</span><a [routerLink]="link()" [queryParams]="qp(c.path)">{{ c.name }}</a>}
|
|
1098
|
+
</nav>
|
|
1099
|
+
<span class="o-r"></span>
|
|
1100
|
+
<button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
|
|
1101
|
+
<span class="anchor">
|
|
1102
|
+
<button type="button" class="o-btn o-sm o-pri" (click)="cloneOpen.set(!cloneOpen())" [attr.aria-expanded]="cloneOpen()" aria-haspopup="dialog" data-testid="clone-menu">Code ⌄</button>
|
|
1103
|
+
@if (cloneOpen()) {
|
|
1104
|
+
<div class="o-menu" role="dialog" aria-label="Clone" style="right:0;top:34px;width:380px;padding:12px;display:flex;flex-direction:column;gap:10px">
|
|
1105
|
+
<b>Clone</b>
|
|
1106
|
+
<div class="o-seg" role="group" aria-label="Protocol" style="align-self:flex-start"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
1107
|
+
<div class="o-copy"><span data-testid="clone-url">{{ cloneUrl() }}</span><button type="button" class="o-btn o-xs" (click)="copyClone()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
1108
|
+
<span class="o-hint">{{ proto() === 'https' ? 'Use a personal access token as the password.' : 'Add your SSH key under Account › SSH and signing keys.' }}</span>
|
|
1109
|
+
<hr style="border:none;border-top:1px solid var(--o-line-soft);margin:0;width:100%">
|
|
1110
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button><button type="button" class="o-btn o-sm" (click)="archive('tar.gz')">Download tar.gz</button></div>
|
|
1111
|
+
</div>
|
|
1112
|
+
}
|
|
1113
|
+
</span>
|
|
1114
|
+
</div>
|
|
1115
|
+
<div class="o-split">
|
|
1116
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
1117
|
+
<div class="o-card">
|
|
1118
|
+
@if (tree()?.lastCommit; as lc) {
|
|
1119
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
1120
|
+
<span class="o-av" aria-hidden="true">{{ initials(lc.author.name) }}</span><b class="o-b5">{{ lc.author.name }}</b>
|
|
1121
|
+
<a class="o-ell o-mut" style="min-width:0" [routerLink]="link('commit/' + lc.oid)">{{ lc.subject }}</a>
|
|
1122
|
+
<span class="o-r o-flex o-sm"><a class="o-mono" [routerLink]="link('commit/' + lc.oid)">{{ lc.short }}</a><span class="o-faint">{{ ago(lc.committer.when) }}</span><a [routerLink]="link('commits')" [queryParams]="{ ref: ref() }">History</a></span>
|
|
1123
|
+
</div>
|
|
1124
|
+
}
|
|
1125
|
+
@if (path()) {<a class="o-row o-click" [routerLink]="link()" [queryParams]="qp(parent())"><span class="o-faint o-mono ic">..</span></a>}
|
|
1126
|
+
@for (e of tree()?.entries ?? []; track e.path) {
|
|
1127
|
+
<a class="o-row o-click" [routerLink]="e.type === 'tree' ? link() : link('blob')" [queryParams]="qp(e.path)" [attr.data-testid]="'entry-' + e.name">
|
|
1128
|
+
<span class="ic" aria-hidden="true">{{ e.type === 'tree' ? '▸' : '·' }}</span>
|
|
1129
|
+
<span class="o-mono o-sm" style="width:200px;flex:none" [class.o-b5]="e.type === 'tree'">{{ e.name }}</span>
|
|
1130
|
+
<span class="o-ell o-faint o-sm" style="flex:1">{{ e.lastCommit?.subject }}</span>
|
|
1131
|
+
<span class="o-faint o-sm">{{ ago(e.lastCommit?.committer?.when) }}</span>
|
|
1132
|
+
</a>
|
|
1133
|
+
} @empty {
|
|
1134
|
+
@if (!tree()) {<div class="o-empty">Loading…</div>}
|
|
1135
|
+
}
|
|
1136
|
+
</div>
|
|
1137
|
+
@if (tree()?.readme; as rd) {
|
|
1138
|
+
<div class="o-card">
|
|
1139
|
+
<div class="o-ch"><b class="o-mono o-sm">{{ rd.path }}</b></div>
|
|
1140
|
+
<div class="o-cb o-md readme" style="font-size:14px;padding:18px 22px" [innerHTML]="rd.html"></div>
|
|
1141
|
+
</div>
|
|
1142
|
+
}
|
|
1143
|
+
</div>
|
|
1144
|
+
<div class="o-col" style="gap:14px">
|
|
1145
|
+
<div class="o-card o-cb o-col"><b>About</b><span class="o-mut o-lh">{{ r.description || 'No description.' }}</span><span class="o-faint o-sm">Default branch <span class="o-mono">{{ r.defaultBranch }}</span></span></div>
|
|
1146
|
+
<div class="o-card o-cb o-col"><div class="o-flex"><b>Releases</b><b class="o-ph-tag o-ga o-r">GA</b></div><span class="o-faint o-sm">Tags and release notes arrive at general availability.</span></div>
|
|
1147
|
+
<div class="o-card o-cb o-col"><b>Deployments</b><span class="o-faint o-sm">Environments show here once Deploy is set up.</span></div>
|
|
1148
|
+
@if (langs().length) {
|
|
1149
|
+
<div class="o-card o-cb o-col">
|
|
1150
|
+
<b>Languages</b>
|
|
1151
|
+
<div style="display:flex;height:8px;border-radius:4px;overflow:hidden" role="img" [attr.aria-label]="langLabel()">@for (l of langs(); track l.name; let i = $index) {<i [style.width.%]="l.percent" [style.background]="colour(i)"></i>}</div>
|
|
1152
|
+
<div class="o-legend">@for (l of langs(); track l.name; let i = $index) {<span><span class="o-dot" [style.background]="colour(i)"></span>{{ l.name }} {{ l.percent.toFixed(0) }}%</span>}</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
}
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
}
|
|
1158
|
+
} @else if (error()) {
|
|
1159
|
+
<div class="o-card o-empty"><p>{{ error() }}</p><a class="o-btn" style="margin-top:14px" [routerLink]="host.link('code')">All repositories</a></div>
|
|
1160
|
+
} @else {
|
|
1161
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1162
|
+
}
|
|
1163
|
+
</div>
|
|
1164
|
+
|
|
1165
|
+
@if (finding()) {
|
|
1166
|
+
<div class="o-overlay" (click)="finding.set(false)">
|
|
1167
|
+
<div class="o-dlg o-wide" role="dialog" aria-modal="true" aria-label="Go to file" (click)="$event.stopPropagation()" (keydown.escape)="finding.set(false)">
|
|
1168
|
+
<div class="o-dlg-b">
|
|
1169
|
+
<input class="o-in o-mono" placeholder="Find a file" aria-label="Find a file" [ngModel]="fq()" (ngModelChange)="fq.set($event)" (keydown.enter)="openFirst()" />
|
|
1170
|
+
<div class="o-col" style="gap:0;max-height:50vh;overflow:auto">
|
|
1171
|
+
@for (f of found(); track f) {<a class="o-row o-click o-mono o-sm" [routerLink]="link('blob')" [queryParams]="qp(f)" (click)="finding.set(false)">{{ f }}</a>}
|
|
1172
|
+
@if (!found().length) {<span class="o-faint o-sm" style="padding:8px">{{ files().length ? 'No files match.' : 'Loading files…' }}</span>}
|
|
1173
|
+
</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
}
|
|
1178
|
+
`, isInline: true, styles: [".anchor{position:relative}.ic{width:18px;flex:none;color:var(--o-dim);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1179
|
+
}
|
|
1180
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoHomePage, decorators: [{
|
|
1181
|
+
type: Component,
|
|
1182
|
+
args: [{ selector: 'o-code-repo', imports: [FormsModule, RouterLink, RefPicker], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1183
|
+
<div class="o-page">
|
|
1184
|
+
@if (repo(); as r) {
|
|
1185
|
+
<div class="o-ph">
|
|
1186
|
+
<div class="o-col">
|
|
1187
|
+
<span class="o-eyebrow">{{ r.projectKey }}</span>
|
|
1188
|
+
<div class="o-flex"><h1 class="o-t o-mono" style="font-size:19px">{{ r.name }}</h1><span class="o-chip">{{ r.visibility === 'private' ? 'Private' : 'Org-internal' }}</span>@if (r.archivedAt) {<span class="o-chip o-amb">Archived</span>}</div>
|
|
1189
|
+
@if (r.description) {<span class="o-mut">{{ r.description }}</span>}
|
|
1190
|
+
</div>
|
|
1191
|
+
@if (!r.empty) {<div class="o-acts"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button></div>}
|
|
1192
|
+
</div>
|
|
1193
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1194
|
+
|
|
1195
|
+
@if (r.importState && r.importState !== 'done') {
|
|
1196
|
+
<div class="o-card o-cb o-col" style="gap:6px">
|
|
1197
|
+
<b>{{ r.importState === 'failed' ? 'The import failed' : 'Importing from ' + r.importSource }}</b>
|
|
1198
|
+
<span class="o-mut o-sm">{{ r.importState === 'failed' ? r.importError : 'This runs in the background; the inbox tells you when it is done.' }}</span>
|
|
1199
|
+
</div>
|
|
1200
|
+
} @else if (r.empty) {
|
|
1201
|
+
<div class="o-card">
|
|
1202
|
+
<div class="o-ch"><b>This repository is empty</b><span class="o-r">Quick setup</span></div>
|
|
1203
|
+
<div class="o-cb o-col" style="gap:14px">
|
|
1204
|
+
<div class="o-flex">
|
|
1205
|
+
<div class="o-seg" role="group" aria-label="Protocol"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
1206
|
+
<div class="o-copy" style="flex:1"><span data-testid="clone-url">{{ cloneUrl() }}</span><button type="button" class="o-btn o-xs" (click)="copyClone()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
1207
|
+
</div>
|
|
1208
|
+
<b class="o-sm">Push an existing repository</b>
|
|
1209
|
+
<div class="o-term"><div>git remote add origin {{ cloneUrl() }}</div><div>git branch -M {{ r.defaultBranch }}</div><div>git push -u origin {{ r.defaultBranch }}</div></div>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
} @else {
|
|
1213
|
+
<div class="o-flex o-wrap">
|
|
1214
|
+
<o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="go($event, '')" />
|
|
1215
|
+
<nav class="o-flex o-sm o-mono" aria-label="Path">
|
|
1216
|
+
<a [routerLink]="link()" [queryParams]="qp('')">{{ r.name }}</a>
|
|
1217
|
+
@for (c of crumbs(); track c.path) {<span class="o-faint">/</span><a [routerLink]="link()" [queryParams]="qp(c.path)">{{ c.name }}</a>}
|
|
1218
|
+
</nav>
|
|
1219
|
+
<span class="o-r"></span>
|
|
1220
|
+
<button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
|
|
1221
|
+
<span class="anchor">
|
|
1222
|
+
<button type="button" class="o-btn o-sm o-pri" (click)="cloneOpen.set(!cloneOpen())" [attr.aria-expanded]="cloneOpen()" aria-haspopup="dialog" data-testid="clone-menu">Code ⌄</button>
|
|
1223
|
+
@if (cloneOpen()) {
|
|
1224
|
+
<div class="o-menu" role="dialog" aria-label="Clone" style="right:0;top:34px;width:380px;padding:12px;display:flex;flex-direction:column;gap:10px">
|
|
1225
|
+
<b>Clone</b>
|
|
1226
|
+
<div class="o-seg" role="group" aria-label="Protocol" style="align-self:flex-start"><button type="button" [class.o-on]="proto() === 'https'" (click)="proto.set('https')">HTTPS</button><button type="button" [class.o-on]="proto() === 'ssh'" (click)="proto.set('ssh')">SSH</button></div>
|
|
1227
|
+
<div class="o-copy"><span data-testid="clone-url">{{ cloneUrl() }}</span><button type="button" class="o-btn o-xs" (click)="copyClone()">{{ copied() ? 'Copied' : 'Copy' }}</button></div>
|
|
1228
|
+
<span class="o-hint">{{ proto() === 'https' ? 'Use a personal access token as the password.' : 'Add your SSH key under Account › SSH and signing keys.' }}</span>
|
|
1229
|
+
<hr style="border:none;border-top:1px solid var(--o-line-soft);margin:0;width:100%">
|
|
1230
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm" (click)="archive('zip')">Download ZIP</button><button type="button" class="o-btn o-sm" (click)="archive('tar.gz')">Download tar.gz</button></div>
|
|
1231
|
+
</div>
|
|
1232
|
+
}
|
|
1233
|
+
</span>
|
|
1234
|
+
</div>
|
|
1235
|
+
<div class="o-split">
|
|
1236
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
1237
|
+
<div class="o-card">
|
|
1238
|
+
@if (tree()?.lastCommit; as lc) {
|
|
1239
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
1240
|
+
<span class="o-av" aria-hidden="true">{{ initials(lc.author.name) }}</span><b class="o-b5">{{ lc.author.name }}</b>
|
|
1241
|
+
<a class="o-ell o-mut" style="min-width:0" [routerLink]="link('commit/' + lc.oid)">{{ lc.subject }}</a>
|
|
1242
|
+
<span class="o-r o-flex o-sm"><a class="o-mono" [routerLink]="link('commit/' + lc.oid)">{{ lc.short }}</a><span class="o-faint">{{ ago(lc.committer.when) }}</span><a [routerLink]="link('commits')" [queryParams]="{ ref: ref() }">History</a></span>
|
|
1243
|
+
</div>
|
|
1244
|
+
}
|
|
1245
|
+
@if (path()) {<a class="o-row o-click" [routerLink]="link()" [queryParams]="qp(parent())"><span class="o-faint o-mono ic">..</span></a>}
|
|
1246
|
+
@for (e of tree()?.entries ?? []; track e.path) {
|
|
1247
|
+
<a class="o-row o-click" [routerLink]="e.type === 'tree' ? link() : link('blob')" [queryParams]="qp(e.path)" [attr.data-testid]="'entry-' + e.name">
|
|
1248
|
+
<span class="ic" aria-hidden="true">{{ e.type === 'tree' ? '▸' : '·' }}</span>
|
|
1249
|
+
<span class="o-mono o-sm" style="width:200px;flex:none" [class.o-b5]="e.type === 'tree'">{{ e.name }}</span>
|
|
1250
|
+
<span class="o-ell o-faint o-sm" style="flex:1">{{ e.lastCommit?.subject }}</span>
|
|
1251
|
+
<span class="o-faint o-sm">{{ ago(e.lastCommit?.committer?.when) }}</span>
|
|
1252
|
+
</a>
|
|
1253
|
+
} @empty {
|
|
1254
|
+
@if (!tree()) {<div class="o-empty">Loading…</div>}
|
|
1255
|
+
}
|
|
1256
|
+
</div>
|
|
1257
|
+
@if (tree()?.readme; as rd) {
|
|
1258
|
+
<div class="o-card">
|
|
1259
|
+
<div class="o-ch"><b class="o-mono o-sm">{{ rd.path }}</b></div>
|
|
1260
|
+
<div class="o-cb o-md readme" style="font-size:14px;padding:18px 22px" [innerHTML]="rd.html"></div>
|
|
1261
|
+
</div>
|
|
1262
|
+
}
|
|
1263
|
+
</div>
|
|
1264
|
+
<div class="o-col" style="gap:14px">
|
|
1265
|
+
<div class="o-card o-cb o-col"><b>About</b><span class="o-mut o-lh">{{ r.description || 'No description.' }}</span><span class="o-faint o-sm">Default branch <span class="o-mono">{{ r.defaultBranch }}</span></span></div>
|
|
1266
|
+
<div class="o-card o-cb o-col"><div class="o-flex"><b>Releases</b><b class="o-ph-tag o-ga o-r">GA</b></div><span class="o-faint o-sm">Tags and release notes arrive at general availability.</span></div>
|
|
1267
|
+
<div class="o-card o-cb o-col"><b>Deployments</b><span class="o-faint o-sm">Environments show here once Deploy is set up.</span></div>
|
|
1268
|
+
@if (langs().length) {
|
|
1269
|
+
<div class="o-card o-cb o-col">
|
|
1270
|
+
<b>Languages</b>
|
|
1271
|
+
<div style="display:flex;height:8px;border-radius:4px;overflow:hidden" role="img" [attr.aria-label]="langLabel()">@for (l of langs(); track l.name; let i = $index) {<i [style.width.%]="l.percent" [style.background]="colour(i)"></i>}</div>
|
|
1272
|
+
<div class="o-legend">@for (l of langs(); track l.name; let i = $index) {<span><span class="o-dot" [style.background]="colour(i)"></span>{{ l.name }} {{ l.percent.toFixed(0) }}%</span>}</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
}
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
}
|
|
1278
|
+
} @else if (error()) {
|
|
1279
|
+
<div class="o-card o-empty"><p>{{ error() }}</p><a class="o-btn" style="margin-top:14px" [routerLink]="host.link('code')">All repositories</a></div>
|
|
1280
|
+
} @else {
|
|
1281
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1282
|
+
}
|
|
1283
|
+
</div>
|
|
1284
|
+
|
|
1285
|
+
@if (finding()) {
|
|
1286
|
+
<div class="o-overlay" (click)="finding.set(false)">
|
|
1287
|
+
<div class="o-dlg o-wide" role="dialog" aria-modal="true" aria-label="Go to file" (click)="$event.stopPropagation()" (keydown.escape)="finding.set(false)">
|
|
1288
|
+
<div class="o-dlg-b">
|
|
1289
|
+
<input class="o-in o-mono" placeholder="Find a file" aria-label="Find a file" [ngModel]="fq()" (ngModelChange)="fq.set($event)" (keydown.enter)="openFirst()" />
|
|
1290
|
+
<div class="o-col" style="gap:0;max-height:50vh;overflow:auto">
|
|
1291
|
+
@for (f of found(); track f) {<a class="o-row o-click o-mono o-sm" [routerLink]="link('blob')" [queryParams]="qp(f)" (click)="finding.set(false)">{{ f }}</a>}
|
|
1292
|
+
@if (!found().length) {<span class="o-faint o-sm" style="padding:8px">{{ files().length ? 'No files match.' : 'Loading files…' }}</span>}
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
</div>
|
|
1297
|
+
}
|
|
1298
|
+
`, styles: [".anchor{position:relative}.ic{width:18px;flex:none;color:var(--o-dim);text-align:center}\n"] }]
|
|
1299
|
+
}], ctorParameters: () => [], propDecorators: { key: [{
|
|
1300
|
+
type: HostListener,
|
|
1301
|
+
args: ['document:keydown', ['$event']]
|
|
1302
|
+
}] } });
|
|
1303
|
+
|
|
1304
|
+
// /{org}/{project}/code/{repo}/blob?ref=&path= (prototype "code_file"): one file, highlighted, with
|
|
1305
|
+
// Markdown preview, blame, raw, copy and download; a line number links to that line (#L12).
|
|
1306
|
+
class FilePage extends RepoPage {
|
|
1307
|
+
constructor() {
|
|
1308
|
+
super();
|
|
1309
|
+
this.router = inject(Router);
|
|
1310
|
+
this.blob = signal(null, ...(ngDevMode ? [{ debugName: "blob" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
this.blame = signal([], ...(ngDevMode ? [{ debugName: "blame" }] : /* istanbul ignore next */ []));
|
|
1312
|
+
this.blaming = signal(false, ...(ngDevMode ? [{ debugName: "blaming" }] : /* istanbul ignore next */ []));
|
|
1313
|
+
this.ref = signal('', ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
|
|
1314
|
+
this.path = signal('', ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
1315
|
+
this.line = signal(0, ...(ngDevMode ? [{ debugName: "line" }] : /* istanbul ignore next */ []));
|
|
1316
|
+
this.view = signal('preview', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
|
|
1317
|
+
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
1318
|
+
this.ago = (s) => ago(s);
|
|
1319
|
+
this.initials = initials;
|
|
1320
|
+
this.crumbs = computed(() => {
|
|
1321
|
+
const parts = this.path().split('/').filter(Boolean);
|
|
1322
|
+
return parts.map((name, i) => ({ name, path: parts.slice(0, i + 1).join('/') }));
|
|
1323
|
+
}, ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
|
|
1324
|
+
this.meta = computed(() => {
|
|
1325
|
+
const b = this.blob();
|
|
1326
|
+
if (!b)
|
|
1327
|
+
return '';
|
|
1328
|
+
return [b.binary ? '' : `${b.lineCount} lines`, size(b.size), b.language ?? ''].filter(Boolean).join(' · ');
|
|
1329
|
+
}, ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
1330
|
+
this.route.queryParamMap.subscribe((q) => {
|
|
1331
|
+
this.ref.set(q.get('ref') ?? '');
|
|
1332
|
+
this.path.set(q.get('path') ?? '');
|
|
1333
|
+
const r = this.repo();
|
|
1334
|
+
if (r && r.name === this.name())
|
|
1335
|
+
this.load(r);
|
|
1336
|
+
});
|
|
1337
|
+
this.route.fragment.subscribe((f) => this.line.set(f?.startsWith('L') ? Number(f.slice(1)) || 0 : 0));
|
|
1338
|
+
}
|
|
1339
|
+
onRepo(r) {
|
|
1340
|
+
this.load(r);
|
|
1341
|
+
}
|
|
1342
|
+
pickRef(ref) {
|
|
1343
|
+
void this.router.navigate(this.link('blob'), { queryParams: { ref, path: this.path() } });
|
|
1344
|
+
}
|
|
1345
|
+
pickLine(n) {
|
|
1346
|
+
this.line.set(n);
|
|
1347
|
+
}
|
|
1348
|
+
copyText() {
|
|
1349
|
+
const b = this.blob();
|
|
1350
|
+
if (!b)
|
|
1351
|
+
return;
|
|
1352
|
+
const div = document.createElement('div');
|
|
1353
|
+
const text = b.lines.map((l) => { div.innerHTML = l; return div.textContent ?? ''; }).join('\n');
|
|
1354
|
+
void copy(text).then(() => { this.copied.set(true); setTimeout(() => this.copied.set(false), 1500); });
|
|
1355
|
+
}
|
|
1356
|
+
toggleBlame() {
|
|
1357
|
+
this.blaming.set(!this.blaming());
|
|
1358
|
+
const r = this.repo();
|
|
1359
|
+
if (this.blaming() && r && !this.blame().length) {
|
|
1360
|
+
this.api.blame(this.projectId(), r.name, this.ref() || r.defaultBranch, this.path()).subscribe({ next: (b) => this.blame.set(b), error: (e) => this.error.set(apiMessage(e, 'Could not load the blame.')) });
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
blameAt(n) {
|
|
1364
|
+
return this.blame().find((h) => n >= h.startLine && n < h.startLine + h.lines);
|
|
1365
|
+
}
|
|
1366
|
+
/** The hunk's commit and author on its first line only. */
|
|
1367
|
+
blameLabel(n) {
|
|
1368
|
+
const h = this.blameAt(n);
|
|
1369
|
+
return h && h.startLine === n ? `${h.commit.slice(0, 7)} ${h.author} · ${ago(h.when)}` : '';
|
|
1370
|
+
}
|
|
1371
|
+
load(r) {
|
|
1372
|
+
if (!this.path())
|
|
1373
|
+
return;
|
|
1374
|
+
this.blob.set(null);
|
|
1375
|
+
this.blame.set([]);
|
|
1376
|
+
this.blaming.set(false);
|
|
1377
|
+
this.api.blob(this.projectId(), r.name, this.ref() || r.defaultBranch, this.path()).subscribe({
|
|
1378
|
+
next: (b) => { this.blob.set(b); this.view.set(b.markdown ? 'preview' : 'code'); if (this.line())
|
|
1379
|
+
setTimeout(() => document.getElementById('L' + this.line())?.scrollIntoView({ block: 'center' })); },
|
|
1380
|
+
error: (e) => this.error.set(apiMessage(e, `Could not open ${this.path()}.`)),
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: FilePage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1384
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: FilePage, isStandalone: true, selector: "o-code-file", usesInheritance: true, ngImport: i0, template: `
|
|
1385
|
+
<div class="o-page o-wide">
|
|
1386
|
+
@if (repo(); as r) {
|
|
1387
|
+
<div class="o-flex o-wrap">
|
|
1388
|
+
<o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="pickRef($event)" />
|
|
1389
|
+
<nav class="o-flex o-sm o-mono" aria-label="Path">
|
|
1390
|
+
<a [routerLink]="link()" [queryParams]="{ ref: ref() || null }">{{ r.name }}</a>
|
|
1391
|
+
@for (c of crumbs(); track c.path; let last = $last) {
|
|
1392
|
+
<span class="o-faint">/</span>
|
|
1393
|
+
@if (last) {<b aria-current="page">{{ c.name }}</b>} @else {<a [routerLink]="link()" [queryParams]="{ ref: ref() || null, path: c.path }">{{ c.name }}</a>}
|
|
1394
|
+
}
|
|
1395
|
+
</nav>
|
|
1396
|
+
</div>
|
|
1397
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1398
|
+
@if (blob(); as b) {
|
|
1399
|
+
<div class="o-card">
|
|
1400
|
+
@if (b.lastCommit; as lc) {
|
|
1401
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
1402
|
+
<span class="o-av" aria-hidden="true">{{ initials(lc.author.name) }}</span><b class="o-b5">{{ lc.author.name }}</b>
|
|
1403
|
+
<span class="o-ell o-mut" style="min-width:0">{{ lc.subject }}</span>
|
|
1404
|
+
<span class="o-r o-sm"><a class="o-mono" [routerLink]="link('commit/' + lc.oid)">{{ lc.short }}</a> · {{ ago(lc.committer.when) }} · <a [routerLink]="link('commits')" [queryParams]="{ ref: ref() || null, path: b.path }">History</a></span>
|
|
1405
|
+
</div>
|
|
1406
|
+
}
|
|
1407
|
+
<div class="o-ch">
|
|
1408
|
+
<span class="o-faint o-sm">{{ meta() }}</span>
|
|
1409
|
+
<span class="o-r o-flex" style="gap:6px">
|
|
1410
|
+
@if (b.markdown) {
|
|
1411
|
+
<div class="o-seg" role="group" aria-label="View"><button type="button" [class.o-on]="view() === 'preview'" (click)="view.set('preview')">Preview</button><button type="button" [class.o-on]="view() === 'code'" (click)="view.set('code')">Code</button></div>
|
|
1412
|
+
}
|
|
1413
|
+
@if (!b.binary && !b.truncated) {<button type="button" class="o-btn o-xs" [class.o-on]="blaming()" [attr.aria-pressed]="blaming()" (click)="toggleBlame()">Blame</button>}
|
|
1414
|
+
@if (b.rawUrl) {<a class="o-btn o-xs" [href]="b.rawUrl" target="_blank" rel="noopener">Raw</a>}
|
|
1415
|
+
@if (!b.binary) {<button type="button" class="o-btn o-xs" (click)="copyText()">{{ copied() ? 'Copied' : 'Copy' }}</button>}
|
|
1416
|
+
@if (b.downloadUrl) {<a class="o-btn o-xs" [href]="b.downloadUrl">Download</a>}
|
|
1417
|
+
</span>
|
|
1418
|
+
</div>
|
|
1419
|
+
@if (b.image && b.rawUrl) {
|
|
1420
|
+
<div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
|
|
1421
|
+
} @else if (b.binary || b.truncated) {
|
|
1422
|
+
<div class="o-empty">{{ b.binary ? 'A binary file.' : 'Too large to show here.' }} @if (b.rawUrl) {<a [href]="b.rawUrl" target="_blank" rel="noopener">View raw</a>}</div>
|
|
1423
|
+
} @else if (b.markdown && view() === 'preview') {
|
|
1424
|
+
<div class="o-cb o-md" style="font-size:14px;padding:18px 22px" [innerHTML]="b.markdown"></div>
|
|
1425
|
+
} @else {
|
|
1426
|
+
<div class="o-code" style="padding:8px 0;overflow-x:auto" role="table" [attr.aria-label]="b.path">
|
|
1427
|
+
@for (l of b.lines; track $index; let i = $index) {
|
|
1428
|
+
<div class="o-ln" [class.o-hl]="line() === i + 1" role="row" [attr.id]="'L' + (i + 1)">
|
|
1429
|
+
@if (blaming()) {<span class="o-bl" role="cell" [attr.title]="blameAt(i + 1)?.summary">{{ blameLabel(i + 1) }}</span>}
|
|
1430
|
+
<a class="o-n" role="rowheader" [routerLink]="[]" [queryParamsHandling]="'preserve'" [fragment]="'L' + (i + 1)" (click)="pickLine(i + 1)" [attr.aria-label]="'Line ' + (i + 1)">{{ i + 1 }}</a>
|
|
1431
|
+
<span class="o-t" role="cell" [innerHTML]="l"></span>
|
|
1432
|
+
</div>
|
|
1433
|
+
}
|
|
1434
|
+
</div>
|
|
1435
|
+
}
|
|
1436
|
+
</div>
|
|
1437
|
+
<span class="o-faint o-sm">Click a line number to link to that line.</span>
|
|
1438
|
+
} @else if (!error()) {
|
|
1439
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1440
|
+
}
|
|
1441
|
+
} @else if (error()) {
|
|
1442
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1443
|
+
}
|
|
1444
|
+
</div>
|
|
1445
|
+
`, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{text-decoration:none}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1446
|
+
}
|
|
1447
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: FilePage, decorators: [{
|
|
1448
|
+
type: Component,
|
|
1449
|
+
args: [{ selector: 'o-code-file', imports: [RouterLink, RefPicker], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1450
|
+
<div class="o-page o-wide">
|
|
1451
|
+
@if (repo(); as r) {
|
|
1452
|
+
<div class="o-flex o-wrap">
|
|
1453
|
+
<o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="pickRef($event)" />
|
|
1454
|
+
<nav class="o-flex o-sm o-mono" aria-label="Path">
|
|
1455
|
+
<a [routerLink]="link()" [queryParams]="{ ref: ref() || null }">{{ r.name }}</a>
|
|
1456
|
+
@for (c of crumbs(); track c.path; let last = $last) {
|
|
1457
|
+
<span class="o-faint">/</span>
|
|
1458
|
+
@if (last) {<b aria-current="page">{{ c.name }}</b>} @else {<a [routerLink]="link()" [queryParams]="{ ref: ref() || null, path: c.path }">{{ c.name }}</a>}
|
|
1459
|
+
}
|
|
1460
|
+
</nav>
|
|
1461
|
+
</div>
|
|
1462
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1463
|
+
@if (blob(); as b) {
|
|
1464
|
+
<div class="o-card">
|
|
1465
|
+
@if (b.lastCommit; as lc) {
|
|
1466
|
+
<div class="o-ch" style="background:var(--o-well)">
|
|
1467
|
+
<span class="o-av" aria-hidden="true">{{ initials(lc.author.name) }}</span><b class="o-b5">{{ lc.author.name }}</b>
|
|
1468
|
+
<span class="o-ell o-mut" style="min-width:0">{{ lc.subject }}</span>
|
|
1469
|
+
<span class="o-r o-sm"><a class="o-mono" [routerLink]="link('commit/' + lc.oid)">{{ lc.short }}</a> · {{ ago(lc.committer.when) }} · <a [routerLink]="link('commits')" [queryParams]="{ ref: ref() || null, path: b.path }">History</a></span>
|
|
1470
|
+
</div>
|
|
1471
|
+
}
|
|
1472
|
+
<div class="o-ch">
|
|
1473
|
+
<span class="o-faint o-sm">{{ meta() }}</span>
|
|
1474
|
+
<span class="o-r o-flex" style="gap:6px">
|
|
1475
|
+
@if (b.markdown) {
|
|
1476
|
+
<div class="o-seg" role="group" aria-label="View"><button type="button" [class.o-on]="view() === 'preview'" (click)="view.set('preview')">Preview</button><button type="button" [class.o-on]="view() === 'code'" (click)="view.set('code')">Code</button></div>
|
|
1477
|
+
}
|
|
1478
|
+
@if (!b.binary && !b.truncated) {<button type="button" class="o-btn o-xs" [class.o-on]="blaming()" [attr.aria-pressed]="blaming()" (click)="toggleBlame()">Blame</button>}
|
|
1479
|
+
@if (b.rawUrl) {<a class="o-btn o-xs" [href]="b.rawUrl" target="_blank" rel="noopener">Raw</a>}
|
|
1480
|
+
@if (!b.binary) {<button type="button" class="o-btn o-xs" (click)="copyText()">{{ copied() ? 'Copied' : 'Copy' }}</button>}
|
|
1481
|
+
@if (b.downloadUrl) {<a class="o-btn o-xs" [href]="b.downloadUrl">Download</a>}
|
|
1482
|
+
</span>
|
|
1483
|
+
</div>
|
|
1484
|
+
@if (b.image && b.rawUrl) {
|
|
1485
|
+
<div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
|
|
1486
|
+
} @else if (b.binary || b.truncated) {
|
|
1487
|
+
<div class="o-empty">{{ b.binary ? 'A binary file.' : 'Too large to show here.' }} @if (b.rawUrl) {<a [href]="b.rawUrl" target="_blank" rel="noopener">View raw</a>}</div>
|
|
1488
|
+
} @else if (b.markdown && view() === 'preview') {
|
|
1489
|
+
<div class="o-cb o-md" style="font-size:14px;padding:18px 22px" [innerHTML]="b.markdown"></div>
|
|
1490
|
+
} @else {
|
|
1491
|
+
<div class="o-code" style="padding:8px 0;overflow-x:auto" role="table" [attr.aria-label]="b.path">
|
|
1492
|
+
@for (l of b.lines; track $index; let i = $index) {
|
|
1493
|
+
<div class="o-ln" [class.o-hl]="line() === i + 1" role="row" [attr.id]="'L' + (i + 1)">
|
|
1494
|
+
@if (blaming()) {<span class="o-bl" role="cell" [attr.title]="blameAt(i + 1)?.summary">{{ blameLabel(i + 1) }}</span>}
|
|
1495
|
+
<a class="o-n" role="rowheader" [routerLink]="[]" [queryParamsHandling]="'preserve'" [fragment]="'L' + (i + 1)" (click)="pickLine(i + 1)" [attr.aria-label]="'Line ' + (i + 1)">{{ i + 1 }}</a>
|
|
1496
|
+
<span class="o-t" role="cell" [innerHTML]="l"></span>
|
|
1497
|
+
</div>
|
|
1498
|
+
}
|
|
1499
|
+
</div>
|
|
1500
|
+
}
|
|
1501
|
+
</div>
|
|
1502
|
+
<span class="o-faint o-sm">Click a line number to link to that line.</span>
|
|
1503
|
+
} @else if (!error()) {
|
|
1504
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1505
|
+
}
|
|
1506
|
+
} @else if (error()) {
|
|
1507
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1508
|
+
}
|
|
1509
|
+
</div>
|
|
1510
|
+
`, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{text-decoration:none}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"] }]
|
|
1511
|
+
}], ctorParameters: () => [] });
|
|
1512
|
+
|
|
1513
|
+
const PAGE = 30;
|
|
1514
|
+
// /{org}/{project}/code/{repo}/commits?ref=&path= (prototype "code_commits"): history grouped by
|
|
1515
|
+
// day, with the issue keys each message names.
|
|
1516
|
+
class CommitsPage extends RepoPage {
|
|
1517
|
+
constructor() {
|
|
1518
|
+
super();
|
|
1519
|
+
this.router = inject(Router);
|
|
1520
|
+
this.commits = signal([], ...(ngDevMode ? [{ debugName: "commits" }] : /* istanbul ignore next */ []));
|
|
1521
|
+
this.ref = signal('', ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
|
|
1522
|
+
this.path = signal('', ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
|
|
1523
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1524
|
+
this.more = signal(false, ...(ngDevMode ? [{ debugName: "more" }] : /* istanbul ignore next */ []));
|
|
1525
|
+
this.ago = (s) => ago(s);
|
|
1526
|
+
this.initials = initials;
|
|
1527
|
+
this.groups = computed(() => {
|
|
1528
|
+
const out = [];
|
|
1529
|
+
for (const c of this.commits()) {
|
|
1530
|
+
const d = dayLabel(c.committer.when);
|
|
1531
|
+
const last = out[out.length - 1];
|
|
1532
|
+
if (last?.day === d)
|
|
1533
|
+
last.items.push(c);
|
|
1534
|
+
else
|
|
1535
|
+
out.push({ day: d, items: [c] });
|
|
1536
|
+
}
|
|
1537
|
+
return out;
|
|
1538
|
+
}, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
1539
|
+
this.route.queryParamMap.subscribe((q) => {
|
|
1540
|
+
this.ref.set(q.get('ref') ?? '');
|
|
1541
|
+
this.path.set(q.get('path') ?? '');
|
|
1542
|
+
const r = this.repo();
|
|
1543
|
+
if (r && r.name === this.name())
|
|
1544
|
+
this.load(r, 0);
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
onRepo(r) {
|
|
1548
|
+
this.load(r, 0);
|
|
1549
|
+
}
|
|
1550
|
+
pickRef(ref) {
|
|
1551
|
+
void this.router.navigate(this.link('commits'), { queryParams: { ref, path: this.path() || null } });
|
|
1552
|
+
}
|
|
1553
|
+
next() {
|
|
1554
|
+
const r = this.repo();
|
|
1555
|
+
if (r)
|
|
1556
|
+
this.load(r, this.commits().length);
|
|
1557
|
+
}
|
|
1558
|
+
load(r, skip) {
|
|
1559
|
+
if (r.empty)
|
|
1560
|
+
return;
|
|
1561
|
+
this.loading.set(true);
|
|
1562
|
+
this.api.commits(this.projectId(), r.name, { ref: this.ref() || r.defaultBranch, path: this.path() || undefined, limit: PAGE, skip }).subscribe({
|
|
1563
|
+
next: (cs) => { this.loading.set(false); this.commits.set(skip ? [...this.commits(), ...cs] : cs); this.more.set(cs.length === PAGE); },
|
|
1564
|
+
error: (e) => { this.loading.set(false); this.error.set(apiMessage(e, 'Could not load the history.')); },
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitsPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1568
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: CommitsPage, isStandalone: true, selector: "o-code-commits", usesInheritance: true, ngImport: i0, template: `
|
|
1569
|
+
<div class="o-page">
|
|
1570
|
+
@if (repo(); as r) {
|
|
1571
|
+
<div class="o-ph">
|
|
1572
|
+
<div class="o-col"><span class="o-eyebrow">{{ r.name }}{{ path() ? ' · ' + path() : '' }}</span><h1 class="o-t">Commits</h1></div>
|
|
1573
|
+
<div class="o-acts"><o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="pickRef($event)" /></div>
|
|
1574
|
+
</div>
|
|
1575
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1576
|
+
@for (g of groups(); track g.day) {
|
|
1577
|
+
<span class="o-lbl" style="margin-top:4px">Commits on {{ g.day }}</span>
|
|
1578
|
+
<div class="o-card">
|
|
1579
|
+
@for (c of g.items; track c.oid) {
|
|
1580
|
+
<div class="o-row" [attr.data-testid]="'commit-' + c.short">
|
|
1581
|
+
<div class="o-col" style="gap:4px;flex:1;min-width:0">
|
|
1582
|
+
<a class="o-b5 o-ell" style="color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>
|
|
1583
|
+
<span class="o-flex o-faint o-sm o-wrap"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span>{{ c.author.name }} committed {{ ago(c.committer.when) }}
|
|
1584
|
+
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
1585
|
+
</span>
|
|
1586
|
+
</div>
|
|
1587
|
+
<a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
|
|
1588
|
+
<a class="o-btn o-sm" [routerLink]="link()" [queryParams]="{ ref: c.oid }" title="Browse files at this commit" [attr.aria-label]="'Browse files at ' + c.short">‹›</a>
|
|
1589
|
+
</div>
|
|
1590
|
+
}
|
|
1591
|
+
</div>
|
|
1592
|
+
} @empty {
|
|
1593
|
+
<div class="o-card"><div class="o-empty">{{ loading() ? 'Loading…' : r.empty ? 'No commits yet: push to ' + r.defaultBranch + '.' : 'No commits.' }}</div></div>
|
|
1594
|
+
}
|
|
1595
|
+
@if (more()) {<button type="button" class="o-btn o-sm" style="align-self:center" (click)="next()" [disabled]="loading()">Older commits</button>}
|
|
1596
|
+
} @else if (error()) {
|
|
1597
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1598
|
+
}
|
|
1599
|
+
</div>
|
|
1600
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1601
|
+
}
|
|
1602
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitsPage, decorators: [{
|
|
1603
|
+
type: Component,
|
|
1604
|
+
args: [{
|
|
1605
|
+
selector: 'o-code-commits',
|
|
1606
|
+
imports: [RouterLink, RefPicker],
|
|
1607
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1608
|
+
template: `
|
|
1609
|
+
<div class="o-page">
|
|
1610
|
+
@if (repo(); as r) {
|
|
1611
|
+
<div class="o-ph">
|
|
1612
|
+
<div class="o-col"><span class="o-eyebrow">{{ r.name }}{{ path() ? ' · ' + path() : '' }}</span><h1 class="o-t">Commits</h1></div>
|
|
1613
|
+
<div class="o-acts"><o-ref-picker [repo]="r.name" [current]="ref() || r.defaultBranch" (picked)="pickRef($event)" /></div>
|
|
1614
|
+
</div>
|
|
1615
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1616
|
+
@for (g of groups(); track g.day) {
|
|
1617
|
+
<span class="o-lbl" style="margin-top:4px">Commits on {{ g.day }}</span>
|
|
1618
|
+
<div class="o-card">
|
|
1619
|
+
@for (c of g.items; track c.oid) {
|
|
1620
|
+
<div class="o-row" [attr.data-testid]="'commit-' + c.short">
|
|
1621
|
+
<div class="o-col" style="gap:4px;flex:1;min-width:0">
|
|
1622
|
+
<a class="o-b5 o-ell" style="color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a>
|
|
1623
|
+
<span class="o-flex o-faint o-sm o-wrap"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span>{{ c.author.name }} committed {{ ago(c.committer.when) }}
|
|
1624
|
+
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
1625
|
+
</span>
|
|
1626
|
+
</div>
|
|
1627
|
+
<a class="o-btn o-sm o-mono" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a>
|
|
1628
|
+
<a class="o-btn o-sm" [routerLink]="link()" [queryParams]="{ ref: c.oid }" title="Browse files at this commit" [attr.aria-label]="'Browse files at ' + c.short">‹›</a>
|
|
1629
|
+
</div>
|
|
1630
|
+
}
|
|
1631
|
+
</div>
|
|
1632
|
+
} @empty {
|
|
1633
|
+
<div class="o-card"><div class="o-empty">{{ loading() ? 'Loading…' : r.empty ? 'No commits yet: push to ' + r.defaultBranch + '.' : 'No commits.' }}</div></div>
|
|
1634
|
+
}
|
|
1635
|
+
@if (more()) {<button type="button" class="o-btn o-sm" style="align-self:center" (click)="next()" [disabled]="loading()">Older commits</button>}
|
|
1636
|
+
} @else if (error()) {
|
|
1637
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1638
|
+
}
|
|
1639
|
+
</div>
|
|
1640
|
+
`,
|
|
1641
|
+
}]
|
|
1642
|
+
}], ctorParameters: () => [] });
|
|
1643
|
+
// /{org}/{project}/code/{repo}/commit/{sha} (prototype "code_commit"): the message, author, keys,
|
|
1644
|
+
// parents and every changed file's diff.
|
|
1645
|
+
class CommitPage extends RepoPage {
|
|
1646
|
+
constructor() {
|
|
1647
|
+
super();
|
|
1648
|
+
this.commit = signal(null, ...(ngDevMode ? [{ debugName: "commit" }] : /* istanbul ignore next */ []));
|
|
1649
|
+
this.ago = (s) => ago(s);
|
|
1650
|
+
this.initials = initials;
|
|
1651
|
+
this.sha = '';
|
|
1652
|
+
this.route.paramMap.subscribe((m) => {
|
|
1653
|
+
this.sha = m.get('sha') ?? '';
|
|
1654
|
+
const r = this.repo();
|
|
1655
|
+
if (r && r.name === this.name())
|
|
1656
|
+
this.onRepo(r);
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
onRepo(r) {
|
|
1660
|
+
if (!this.sha)
|
|
1661
|
+
return;
|
|
1662
|
+
this.commit.set(null);
|
|
1663
|
+
this.api.commit(this.projectId(), r.name, this.sha).subscribe({ next: (c) => this.commit.set(c), error: (e) => this.error.set(apiMessage(e, `Could not open commit ${this.sha.slice(0, 7)}.`)) });
|
|
1664
|
+
}
|
|
1665
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1666
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: CommitPage, isStandalone: true, selector: "o-code-commit", usesInheritance: true, ngImport: i0, template: `
|
|
1667
|
+
<div class="o-page">
|
|
1668
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1669
|
+
@if (commit(); as c) {
|
|
1670
|
+
<div class="o-card">
|
|
1671
|
+
<div class="o-cb o-col" style="gap:10px">
|
|
1672
|
+
<h1 class="o-t">{{ c.subject }}</h1>
|
|
1673
|
+
@if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
|
|
1674
|
+
<div class="o-flex o-wrap o-sm">
|
|
1675
|
+
<span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><b class="o-b5">{{ c.author.name }}</b><span class="o-faint">committed {{ ago(c.committer.when) }}</span>
|
|
1676
|
+
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
1677
|
+
<span class="o-r o-mono o-faint">commit {{ c.oid.slice(0, 10) }}@for (p of c.parents; track p) { · parent <a [routerLink]="link('commit/' + p)">{{ p.slice(0, 7) }}</a>}</span>
|
|
1678
|
+
</div>
|
|
1679
|
+
</div>
|
|
1680
|
+
<div class="o-cf"><a [routerLink]="link()" [queryParams]="{ ref: c.oid }">Browse files</a><span class="o-r">{{ c.files.length }} {{ c.files.length === 1 ? 'file' : 'files' }} changed · <span class="o-grn-t">+{{ c.additions }}</span> <span class="o-red-t">−{{ c.deletions }}</span></span></div>
|
|
1681
|
+
</div>
|
|
1682
|
+
@for (f of c.files; track f.path) {
|
|
1683
|
+
<o-diff [repo]="name()" [file]="f" [head]="c.oid" />
|
|
1684
|
+
}
|
|
1685
|
+
} @else if (!error()) {
|
|
1686
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1687
|
+
}
|
|
1688
|
+
</div>
|
|
1689
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment"], outputs: ["comment", "reply", "resolve"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1690
|
+
}
|
|
1691
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitPage, decorators: [{
|
|
1692
|
+
type: Component,
|
|
1693
|
+
args: [{
|
|
1694
|
+
selector: 'o-code-commit',
|
|
1695
|
+
imports: [RouterLink, DiffView],
|
|
1696
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1697
|
+
template: `
|
|
1698
|
+
<div class="o-page">
|
|
1699
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1700
|
+
@if (commit(); as c) {
|
|
1701
|
+
<div class="o-card">
|
|
1702
|
+
<div class="o-cb o-col" style="gap:10px">
|
|
1703
|
+
<h1 class="o-t">{{ c.subject }}</h1>
|
|
1704
|
+
@if (c.body) {<span class="o-mut o-lh" style="white-space:pre-wrap">{{ c.body }}</span>}
|
|
1705
|
+
<div class="o-flex o-wrap o-sm">
|
|
1706
|
+
<span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><b class="o-b5">{{ c.author.name }}</b><span class="o-faint">committed {{ ago(c.committer.when) }}</span>
|
|
1707
|
+
@for (k of c.keys; track k) {<a class="o-chip o-blu" [routerLink]="host.link('issues/' + k)">{{ k }}</a>}
|
|
1708
|
+
<span class="o-r o-mono o-faint">commit {{ c.oid.slice(0, 10) }}@for (p of c.parents; track p) { · parent <a [routerLink]="link('commit/' + p)">{{ p.slice(0, 7) }}</a>}</span>
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
<div class="o-cf"><a [routerLink]="link()" [queryParams]="{ ref: c.oid }">Browse files</a><span class="o-r">{{ c.files.length }} {{ c.files.length === 1 ? 'file' : 'files' }} changed · <span class="o-grn-t">+{{ c.additions }}</span> <span class="o-red-t">−{{ c.deletions }}</span></span></div>
|
|
1712
|
+
</div>
|
|
1713
|
+
@for (f of c.files; track f.path) {
|
|
1714
|
+
<o-diff [repo]="name()" [file]="f" [head]="c.oid" />
|
|
1715
|
+
}
|
|
1716
|
+
} @else if (!error()) {
|
|
1717
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1718
|
+
}
|
|
1719
|
+
</div>
|
|
1720
|
+
`,
|
|
1721
|
+
}]
|
|
1722
|
+
}], ctorParameters: () => [] });
|
|
1723
|
+
// /{org}/{project}/code/{repo}/branches (prototype "code_branches"): each branch with its last
|
|
1724
|
+
// commit, how far behind and ahead of the default branch it is, protection, its pull request, and
|
|
1725
|
+
// create, delete and delete-merged.
|
|
1726
|
+
class BranchesPage extends RepoPage {
|
|
1727
|
+
constructor() {
|
|
1728
|
+
super(...arguments);
|
|
1729
|
+
this.refs = signal([], ...(ngDevMode ? [{ debugName: "refs" }] : /* istanbul ignore next */ []));
|
|
1730
|
+
this.pulls = signal([], ...(ngDevMode ? [{ debugName: "pulls" }] : /* istanbul ignore next */ []));
|
|
1731
|
+
this.adding = signal(false, ...(ngDevMode ? [{ debugName: "adding" }] : /* istanbul ignore next */ []));
|
|
1732
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
1733
|
+
this.ago = (s) => ago(s);
|
|
1734
|
+
this.initials = initials;
|
|
1735
|
+
this.newName = '';
|
|
1736
|
+
this.from = '';
|
|
1737
|
+
this.maxAB = computed(() => Math.max(1, ...this.refs().map((r) => Math.max(r.ahead, r.behind))), ...(ngDevMode ? [{ debugName: "maxAB" }] : /* istanbul ignore next */ []));
|
|
1738
|
+
}
|
|
1739
|
+
onRepo(r) {
|
|
1740
|
+
this.from = r.defaultBranch;
|
|
1741
|
+
this.load(r);
|
|
1742
|
+
}
|
|
1743
|
+
bar(n) {
|
|
1744
|
+
return n ? Math.max(4, (100 * n) / this.maxAB()) : 0;
|
|
1745
|
+
}
|
|
1746
|
+
pullFor(branch) {
|
|
1747
|
+
return this.pulls().find((p) => p.source === branch && p.state === 'open') ?? this.pulls().find((p) => p.source === branch);
|
|
1748
|
+
}
|
|
1749
|
+
create() {
|
|
1750
|
+
const r = this.repo();
|
|
1751
|
+
const n = this.newName.trim();
|
|
1752
|
+
if (!r || !n)
|
|
1753
|
+
return;
|
|
1754
|
+
this.busy.set(true);
|
|
1755
|
+
this.api.createBranch(this.projectId(), r.name, n, this.from || r.defaultBranch).subscribe({
|
|
1756
|
+
next: () => { this.busy.set(false); this.adding.set(false); this.newName = ''; this.note.set(`Branch ${n} created.`); this.load(r); },
|
|
1757
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not create the branch.')); },
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
remove(b) {
|
|
1761
|
+
const r = this.repo();
|
|
1762
|
+
if (!r)
|
|
1763
|
+
return;
|
|
1764
|
+
this.api.deleteBranch(this.projectId(), r.name, b.name).subscribe({
|
|
1765
|
+
next: () => { this.note.set(`Branch ${b.name} deleted.`); this.load(r); },
|
|
1766
|
+
error: (e) => this.error.set(apiMessage(e, `Could not delete ${b.name}.`)),
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
deleteMerged() {
|
|
1770
|
+
const r = this.repo();
|
|
1771
|
+
if (!r)
|
|
1772
|
+
return;
|
|
1773
|
+
this.busy.set(true);
|
|
1774
|
+
this.api.deleteMerged(this.projectId(), r.name).subscribe({
|
|
1775
|
+
next: (res) => { this.busy.set(false); const n = (res?.deleted ?? res ?? []).length ?? 0; this.note.set(n ? `${n} merged ${n === 1 ? 'branch' : 'branches'} deleted.` : 'No merged branches to delete.'); this.load(r); },
|
|
1776
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not delete merged branches.')); },
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
load(r) {
|
|
1780
|
+
if (r.empty)
|
|
1781
|
+
return;
|
|
1782
|
+
this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.refs.set(b), error: (e) => this.error.set(apiMessage(e, 'Could not load the branches.')) });
|
|
1783
|
+
this.api.pulls(this.projectId(), r.name, { state: 'all' }).subscribe({ next: (p) => this.pulls.set(p), error: () => undefined });
|
|
1784
|
+
}
|
|
1785
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BranchesPage, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
1786
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: BranchesPage, isStandalone: true, selector: "o-code-branches", usesInheritance: true, ngImport: i0, template: `
|
|
1787
|
+
<div class="o-page">
|
|
1788
|
+
@if (repo(); as r) {
|
|
1789
|
+
<div class="o-ph">
|
|
1790
|
+
<div class="o-col"><span class="o-eyebrow">{{ r.name }}</span><h1 class="o-t">Branches</h1></div>
|
|
1791
|
+
@if (canWrite()) {
|
|
1792
|
+
<div class="o-acts">
|
|
1793
|
+
<button type="button" class="o-btn o-sm" (click)="deleteMerged()" [disabled]="busy()">Delete merged</button>
|
|
1794
|
+
<button type="button" class="o-btn o-sm o-dark" (click)="adding.set(!adding())" data-testid="new-branch">+ New branch</button>
|
|
1795
|
+
</div>
|
|
1796
|
+
}
|
|
1797
|
+
</div>
|
|
1798
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1799
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
1800
|
+
@if (adding()) {
|
|
1801
|
+
<form class="o-card o-cb o-flex o-wrap" (ngSubmit)="create()">
|
|
1802
|
+
<div class="o-fld" style="flex:1"><label for="nb-name">Branch name</label><input id="nb-name" class="o-in o-mono" name="name" [(ngModel)]="newName" data-testid="branch-name" /></div>
|
|
1803
|
+
<div class="o-fld"><label for="nb-from">From</label><select id="nb-from" class="o-in" name="from" [(ngModel)]="from">@for (b of refs(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}</select></div>
|
|
1804
|
+
<button type="submit" class="o-btn o-dark" style="align-self:flex-end" [disabled]="!newName.trim() || busy()" data-testid="branch-create">Create branch</button>
|
|
1805
|
+
</form>
|
|
1806
|
+
}
|
|
1807
|
+
<div class="o-card" style="overflow-x:auto">
|
|
1808
|
+
<div class="o-tbl" role="table" aria-label="Branches" style="min-width:720px">
|
|
1809
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Branch</div><div class="o-th" role="columnheader">Updated</div><div class="o-th" role="columnheader" style="width:220px">Behind | Ahead of {{ r.defaultBranch }}</div><div class="o-th" role="columnheader">Pull request</div><div class="o-th" role="columnheader"></div></div></div>
|
|
1810
|
+
<div class="o-tbody" role="rowgroup">
|
|
1811
|
+
@for (b of refs(); track b.name) {
|
|
1812
|
+
<div class="o-tr" role="row" [attr.data-testid]="'branch-' + b.name">
|
|
1813
|
+
<div class="o-td" role="cell"><span class="o-flex"><a class="o-mono o-b5 o-sm" [routerLink]="link()" [queryParams]="{ ref: b.name }">{{ b.name }}</a>@if (b.default) {<span class="o-chip">default</span>}@if (b.protected) {<span class="o-chip o-amb" [attr.title]="'Rule ' + b.rule">protected</span>}@if (b.merged && !b.default) {<span class="o-chip o-vio">merged</span>}</span></div>
|
|
1814
|
+
<div class="o-td o-mut o-sm" role="cell">@if (b.commit) {<span class="o-flex"><span class="o-av" aria-hidden="true">{{ initials(b.commit.author.name) }}</span>{{ ago(b.commit.committer.when) }}</span>}</div>
|
|
1815
|
+
<div class="o-td" role="cell">
|
|
1816
|
+
@if (!b.default) {
|
|
1817
|
+
<div class="ab" [attr.aria-label]="b.behind + ' behind, ' + b.ahead + ' ahead'">
|
|
1818
|
+
<span class="o-mono o-xs o-faint" style="width:26px;text-align:right;padding-right:4px">{{ b.behind }}</span>
|
|
1819
|
+
<div class="l"><i [style.width.%]="bar(b.behind)" style="border-radius:3px 0 0 3px;background:var(--o-line-mid)"></i></div>
|
|
1820
|
+
<div class="rr"><i [style.width.%]="bar(b.ahead)" style="border-radius:0 3px 3px 0;background:var(--o-acc)"></i></div>
|
|
1821
|
+
<span class="o-mono o-xs o-faint" style="width:26px;padding-left:4px">{{ b.ahead }}</span>
|
|
1822
|
+
</div>
|
|
1823
|
+
}
|
|
1824
|
+
</div>
|
|
1825
|
+
<div class="o-td" role="cell">
|
|
1826
|
+
@if (pullFor(b.name); as pr) {<a [class]="pr.state === 'merged' ? 'o-chip o-vio' : pr.state === 'closed' ? 'o-chip' : 'o-chip o-blu'" [routerLink]="link('pull/' + pr.number)">#{{ pr.number }} {{ pr.state }}</a>}
|
|
1827
|
+
@else if (!b.default && b.ahead > 0 && canWrite()) {<a class="o-btn o-xs" [routerLink]="link('pulls')" [queryParams]="{ new: 1, source: b.name }">New PR</a>}
|
|
1828
|
+
</div>
|
|
1829
|
+
<div class="o-td o-num" role="cell">@if (b.canDelete && canWrite()) {<button type="button" class="o-btn o-xs o-ghost" (click)="remove(b)" [attr.aria-label]="'Delete ' + b.name">Delete</button>}</div>
|
|
1830
|
+
</div>
|
|
1831
|
+
} @empty {
|
|
1832
|
+
<div class="o-empty">{{ r.empty ? 'No branches yet: push to create ' + r.defaultBranch + '.' : 'Loading…' }}</div>
|
|
1833
|
+
}
|
|
1834
|
+
</div>
|
|
1835
|
+
</div>
|
|
1836
|
+
</div>
|
|
1837
|
+
} @else if (error()) {
|
|
1838
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1839
|
+
}
|
|
1840
|
+
</div>
|
|
1841
|
+
`, isInline: true, styles: [".ab{display:flex;align-items:center;gap:0;width:200px}.ab i{display:block;height:6px}.ab .l{flex:1;display:flex;justify-content:flex-end;border-right:1px solid var(--o-line-mid)}.ab .rr{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1842
|
+
}
|
|
1843
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BranchesPage, decorators: [{
|
|
1844
|
+
type: Component,
|
|
1845
|
+
args: [{ selector: 'o-code-branches', imports: [FormsModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1846
|
+
<div class="o-page">
|
|
1847
|
+
@if (repo(); as r) {
|
|
1848
|
+
<div class="o-ph">
|
|
1849
|
+
<div class="o-col"><span class="o-eyebrow">{{ r.name }}</span><h1 class="o-t">Branches</h1></div>
|
|
1850
|
+
@if (canWrite()) {
|
|
1851
|
+
<div class="o-acts">
|
|
1852
|
+
<button type="button" class="o-btn o-sm" (click)="deleteMerged()" [disabled]="busy()">Delete merged</button>
|
|
1853
|
+
<button type="button" class="o-btn o-sm o-dark" (click)="adding.set(!adding())" data-testid="new-branch">+ New branch</button>
|
|
1854
|
+
</div>
|
|
1855
|
+
}
|
|
1856
|
+
</div>
|
|
1857
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
1858
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
1859
|
+
@if (adding()) {
|
|
1860
|
+
<form class="o-card o-cb o-flex o-wrap" (ngSubmit)="create()">
|
|
1861
|
+
<div class="o-fld" style="flex:1"><label for="nb-name">Branch name</label><input id="nb-name" class="o-in o-mono" name="name" [(ngModel)]="newName" data-testid="branch-name" /></div>
|
|
1862
|
+
<div class="o-fld"><label for="nb-from">From</label><select id="nb-from" class="o-in" name="from" [(ngModel)]="from">@for (b of refs(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}</select></div>
|
|
1863
|
+
<button type="submit" class="o-btn o-dark" style="align-self:flex-end" [disabled]="!newName.trim() || busy()" data-testid="branch-create">Create branch</button>
|
|
1864
|
+
</form>
|
|
1865
|
+
}
|
|
1866
|
+
<div class="o-card" style="overflow-x:auto">
|
|
1867
|
+
<div class="o-tbl" role="table" aria-label="Branches" style="min-width:720px">
|
|
1868
|
+
<div class="o-thead" role="rowgroup"><div class="o-tr" role="row"><div class="o-th" role="columnheader">Branch</div><div class="o-th" role="columnheader">Updated</div><div class="o-th" role="columnheader" style="width:220px">Behind | Ahead of {{ r.defaultBranch }}</div><div class="o-th" role="columnheader">Pull request</div><div class="o-th" role="columnheader"></div></div></div>
|
|
1869
|
+
<div class="o-tbody" role="rowgroup">
|
|
1870
|
+
@for (b of refs(); track b.name) {
|
|
1871
|
+
<div class="o-tr" role="row" [attr.data-testid]="'branch-' + b.name">
|
|
1872
|
+
<div class="o-td" role="cell"><span class="o-flex"><a class="o-mono o-b5 o-sm" [routerLink]="link()" [queryParams]="{ ref: b.name }">{{ b.name }}</a>@if (b.default) {<span class="o-chip">default</span>}@if (b.protected) {<span class="o-chip o-amb" [attr.title]="'Rule ' + b.rule">protected</span>}@if (b.merged && !b.default) {<span class="o-chip o-vio">merged</span>}</span></div>
|
|
1873
|
+
<div class="o-td o-mut o-sm" role="cell">@if (b.commit) {<span class="o-flex"><span class="o-av" aria-hidden="true">{{ initials(b.commit.author.name) }}</span>{{ ago(b.commit.committer.when) }}</span>}</div>
|
|
1874
|
+
<div class="o-td" role="cell">
|
|
1875
|
+
@if (!b.default) {
|
|
1876
|
+
<div class="ab" [attr.aria-label]="b.behind + ' behind, ' + b.ahead + ' ahead'">
|
|
1877
|
+
<span class="o-mono o-xs o-faint" style="width:26px;text-align:right;padding-right:4px">{{ b.behind }}</span>
|
|
1878
|
+
<div class="l"><i [style.width.%]="bar(b.behind)" style="border-radius:3px 0 0 3px;background:var(--o-line-mid)"></i></div>
|
|
1879
|
+
<div class="rr"><i [style.width.%]="bar(b.ahead)" style="border-radius:0 3px 3px 0;background:var(--o-acc)"></i></div>
|
|
1880
|
+
<span class="o-mono o-xs o-faint" style="width:26px;padding-left:4px">{{ b.ahead }}</span>
|
|
1881
|
+
</div>
|
|
1882
|
+
}
|
|
1883
|
+
</div>
|
|
1884
|
+
<div class="o-td" role="cell">
|
|
1885
|
+
@if (pullFor(b.name); as pr) {<a [class]="pr.state === 'merged' ? 'o-chip o-vio' : pr.state === 'closed' ? 'o-chip' : 'o-chip o-blu'" [routerLink]="link('pull/' + pr.number)">#{{ pr.number }} {{ pr.state }}</a>}
|
|
1886
|
+
@else if (!b.default && b.ahead > 0 && canWrite()) {<a class="o-btn o-xs" [routerLink]="link('pulls')" [queryParams]="{ new: 1, source: b.name }">New PR</a>}
|
|
1887
|
+
</div>
|
|
1888
|
+
<div class="o-td o-num" role="cell">@if (b.canDelete && canWrite()) {<button type="button" class="o-btn o-xs o-ghost" (click)="remove(b)" [attr.aria-label]="'Delete ' + b.name">Delete</button>}</div>
|
|
1889
|
+
</div>
|
|
1890
|
+
} @empty {
|
|
1891
|
+
<div class="o-empty">{{ r.empty ? 'No branches yet: push to create ' + r.defaultBranch + '.' : 'Loading…' }}</div>
|
|
1892
|
+
}
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
</div>
|
|
1896
|
+
} @else if (error()) {
|
|
1897
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
1898
|
+
}
|
|
1899
|
+
</div>
|
|
1900
|
+
`, styles: [".ab{display:flex;align-items:center;gap:0;width:200px}.ab i{display:block;height:6px}.ab .l{flex:1;display:flex;justify-content:flex-end;border-right:1px solid var(--o-line-mid)}.ab .rr{flex:1}\n"] }]
|
|
1901
|
+
}] });
|
|
1902
|
+
|
|
1903
|
+
const REVIEW = { approved: 'Approved', changes_requested: 'Changes requested', review_required: 'Review required', commented: 'Commented', '': '' };
|
|
1904
|
+
// /{org}/{project}/code/pulls (every repository) and /code/{repo}/pulls (prototype "code_pulls"):
|
|
1905
|
+
// open, merged and closed pull requests with their checks, review state and comments; filters; and
|
|
1906
|
+
// the form that opens one (?new=1&source=).
|
|
1907
|
+
class PullsPage {
|
|
1908
|
+
constructor() {
|
|
1909
|
+
this.host = inject(CodeHost);
|
|
1910
|
+
this.api = inject(CodeApi);
|
|
1911
|
+
this.route = inject(ActivatedRoute);
|
|
1912
|
+
this.router = inject(Router);
|
|
1913
|
+
this.states = [{ id: 'open', label: 'Open', icon: '●' }, { id: 'merged', label: 'Merged', icon: '✓' }, { id: 'closed', label: 'Closed', icon: '✕' }];
|
|
1914
|
+
this.repoName = signal('', ...(ngDevMode ? [{ debugName: "repoName" }] : /* istanbul ignore next */ []));
|
|
1915
|
+
this.repos = signal([], ...(ngDevMode ? [{ debugName: "repos" }] : /* istanbul ignore next */ []));
|
|
1916
|
+
this.rows = signal([], ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
1917
|
+
this.state = signal('open', ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
1918
|
+
this.mine = signal(false, ...(ngDevMode ? [{ debugName: "mine" }] : /* istanbul ignore next */ []));
|
|
1919
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1920
|
+
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
1921
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
1922
|
+
this.opening = signal(false, ...(ngDevMode ? [{ debugName: "opening" }] : /* istanbul ignore next */ []));
|
|
1923
|
+
this.newRepo = signal('', ...(ngDevMode ? [{ debugName: "newRepo" }] : /* istanbul ignore next */ []));
|
|
1924
|
+
this.branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
|
|
1925
|
+
this.source = signal('', ...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
|
|
1926
|
+
this.target = signal('', ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
|
|
1927
|
+
this.cmp = signal(null, ...(ngDevMode ? [{ debugName: "cmp" }] : /* istanbul ignore next */ []));
|
|
1928
|
+
this.q = '';
|
|
1929
|
+
this.author = '';
|
|
1930
|
+
this.title = '';
|
|
1931
|
+
this.body = '';
|
|
1932
|
+
this.reviewer = '';
|
|
1933
|
+
this.draft = false;
|
|
1934
|
+
this.wantNew = null;
|
|
1935
|
+
this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
|
|
1936
|
+
this.ago = (s) => ago(s);
|
|
1937
|
+
this.dot = checkDot;
|
|
1938
|
+
this.review = (r) => REVIEW[r] ?? r.replace(/_/g, ' ');
|
|
1939
|
+
this.canWrite = computed(() => {
|
|
1940
|
+
const p = this.host.project();
|
|
1941
|
+
return !!p && !p.archivedAt && p.capabilities.includes('code.write');
|
|
1942
|
+
}, ...(ngDevMode ? [{ debugName: "canWrite" }] : /* istanbul ignore next */ []));
|
|
1943
|
+
this.route.paramMap.subscribe((m) => this.repoName.set(m.get('repo') ?? ''));
|
|
1944
|
+
this.route.queryParamMap.subscribe((q) => { if (q.get('new'))
|
|
1945
|
+
this.wantNew = { source: q.get('source') ?? '' }; });
|
|
1946
|
+
effect(() => {
|
|
1947
|
+
const p = this.host.project();
|
|
1948
|
+
this.repoName();
|
|
1949
|
+
if (p)
|
|
1950
|
+
untracked(() => this.api.repos(p.id).subscribe({
|
|
1951
|
+
next: (rs) => {
|
|
1952
|
+
const only = this.repoName();
|
|
1953
|
+
this.repos.set(only ? rs.filter((r) => r.name === only) : rs.filter((r) => !r.empty));
|
|
1954
|
+
this.load();
|
|
1955
|
+
if (this.wantNew) {
|
|
1956
|
+
this.startNew(this.wantNew.source);
|
|
1957
|
+
this.wantNew = null;
|
|
1958
|
+
}
|
|
1959
|
+
},
|
|
1960
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not load the repositories.')),
|
|
1961
|
+
}));
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
load() {
|
|
1965
|
+
const p = this.host.project();
|
|
1966
|
+
if (!p)
|
|
1967
|
+
return;
|
|
1968
|
+
const q = { state: this.state(), q: this.q.trim(), author: this.author, needsMyReview: this.mine() };
|
|
1969
|
+
const repos = this.repos();
|
|
1970
|
+
if (!repos.length) {
|
|
1971
|
+
this.rows.set([]);
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
this.loading.set(true);
|
|
1975
|
+
forkJoin(repos.map((r) => this.api.pulls(p.id, r.name, q).pipe(map$1((ps) => ps.map((x) => ({ ...x, repo: r.name }))), catchError(() => of([]))))).subscribe({
|
|
1976
|
+
next: (lists) => {
|
|
1977
|
+
this.loading.set(false);
|
|
1978
|
+
this.rows.set(lists.flat().sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '')));
|
|
1979
|
+
},
|
|
1980
|
+
error: (e) => { this.loading.set(false); this.error.set(apiMessage(e, 'Could not load the pull requests.')); },
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
startNew(source) {
|
|
1984
|
+
const repo = this.repoName() || this.repos()[0]?.name || '';
|
|
1985
|
+
if (!repo)
|
|
1986
|
+
return;
|
|
1987
|
+
this.opening.set(true);
|
|
1988
|
+
this.title = '';
|
|
1989
|
+
this.body = '';
|
|
1990
|
+
this.pickRepo(repo, source);
|
|
1991
|
+
}
|
|
1992
|
+
pickRepo(name, source = '') {
|
|
1993
|
+
const p = this.host.project();
|
|
1994
|
+
const r = this.repos().find((x) => x.name === name);
|
|
1995
|
+
if (!p || !r)
|
|
1996
|
+
return;
|
|
1997
|
+
this.newRepo.set(name);
|
|
1998
|
+
this.target.set(r.defaultBranch);
|
|
1999
|
+
this.source.set(source);
|
|
2000
|
+
this.cmp.set(null);
|
|
2001
|
+
this.api.refs(p.id, name, 'branch').subscribe({ next: (b) => { this.branches.set(b); this.compare(); }, error: () => undefined });
|
|
2002
|
+
this.api.template(p.id, name, r.defaultBranch).subscribe({ next: (t) => { if (!this.body)
|
|
2003
|
+
this.body = t; }, error: () => undefined });
|
|
2004
|
+
}
|
|
2005
|
+
compare() {
|
|
2006
|
+
const p = this.host.project();
|
|
2007
|
+
if (!p || !this.source() || !this.target()) {
|
|
2008
|
+
this.cmp.set(null);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (!this.title.trim())
|
|
2012
|
+
this.title = this.source().replace(/^[^/]+\//, '').replace(/[-_]/g, ' ');
|
|
2013
|
+
this.api.compare(p.id, this.newRepo(), this.target(), this.source()).subscribe({ next: (c) => this.cmp.set(c), error: () => this.cmp.set(null) });
|
|
2014
|
+
}
|
|
2015
|
+
open() {
|
|
2016
|
+
const p = this.host.project();
|
|
2017
|
+
if (!p)
|
|
2018
|
+
return;
|
|
2019
|
+
this.busy.set(true);
|
|
2020
|
+
this.error.set('');
|
|
2021
|
+
this.api.openPull(p.id, this.newRepo(), { source: this.source(), target: this.target(), title: this.title.trim(), body: this.body, draft: this.draft, reviewers: this.reviewer ? [this.reviewer] : [] }).subscribe({
|
|
2022
|
+
next: (pr) => { this.busy.set(false); void this.router.navigate(this.host.link(`code/${this.newRepo()}/pull/${pr.number}`)); },
|
|
2023
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not open the pull request.')); },
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullsPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2027
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: PullsPage, isStandalone: true, selector: "o-code-pulls", ngImport: i0, template: `
|
|
2028
|
+
<div class="o-page">
|
|
2029
|
+
<div class="o-ph">
|
|
2030
|
+
<div class="o-col"><span class="o-eyebrow">{{ repoName() || host.project()?.key + ' · every repository' }}</span><h1 class="o-t">Pull requests</h1></div>
|
|
2031
|
+
@if (canWrite() && repos().length) {<div class="o-acts"><button type="button" class="o-btn o-dark" (click)="startNew('')" data-testid="new-pull">New pull request</button></div>}
|
|
2032
|
+
</div>
|
|
2033
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2034
|
+
|
|
2035
|
+
@if (opening()) {
|
|
2036
|
+
<form class="o-card" (ngSubmit)="open()" aria-label="New pull request">
|
|
2037
|
+
<div class="o-ch"><b>New pull request</b></div>
|
|
2038
|
+
<div class="o-cb o-col" style="gap:12px">
|
|
2039
|
+
<div class="o-flex o-wrap">
|
|
2040
|
+
@if (!repoName()) {
|
|
2041
|
+
<select class="o-in o-sm" style="width:auto" name="repo" aria-label="Repository" [ngModel]="newRepo()" (ngModelChange)="pickRepo($event)">@for (r of repos(); track r.name) {<option [value]="r.name">{{ r.name }}</option>}</select>
|
|
2042
|
+
}
|
|
2043
|
+
<span class="o-faint o-sm">into</span>
|
|
2044
|
+
<select class="o-in o-sm o-mono" style="width:auto" name="target" aria-label="Target branch" [ngModel]="target()" (ngModelChange)="target.set($event); compare()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}</select>
|
|
2045
|
+
<span class="o-faint o-sm">from</span>
|
|
2046
|
+
<select class="o-in o-sm o-mono" style="width:auto" name="source" aria-label="Source branch" [ngModel]="source()" (ngModelChange)="source.set($event); compare()" data-testid="pull-source">
|
|
2047
|
+
<option value="">Choose a branch</option>
|
|
2048
|
+
@for (b of branches(); track b.name) {@if (b.name !== target()) {<option [value]="b.name">{{ b.name }}</option>}}
|
|
2049
|
+
</select>
|
|
2050
|
+
@if (cmp(); as c) {<span class="o-faint o-sm">{{ c.ahead }} {{ c.ahead === 1 ? 'commit' : 'commits' }} · {{ c.files.length }} {{ c.files.length === 1 ? 'file' : 'files' }}</span>}
|
|
2051
|
+
</div>
|
|
2052
|
+
<div class="o-fld"><label for="np-title">Title</label><input id="np-title" class="o-in" name="title" [(ngModel)]="title" data-testid="pull-title" /><span class="o-hint">Put an issue key in the title or description to link it; “Closes PAY-142” closes it on merge.</span></div>
|
|
2053
|
+
<div class="o-fld"><label for="np-body">Description</label><textarea id="np-body" class="o-in" rows="5" name="body" [(ngModel)]="body" style="height:auto;padding:8px 10px"></textarea></div>
|
|
2054
|
+
<div class="o-fld"><label for="np-rev">Reviewers</label>
|
|
2055
|
+
<select id="np-rev" class="o-in" name="reviewer" [(ngModel)]="reviewer"><option value="">Nobody yet</option>@for (p of host.people(); track p.id) {@if (p.id !== host.meId()) {<option [value]="p.id">{{ p.name || p.email }}</option>}}</select>
|
|
2056
|
+
</div>
|
|
2057
|
+
</div>
|
|
2058
|
+
<div class="o-cf"><label class="o-flex o-sm"><input type="checkbox" name="draft" [(ngModel)]="draft" />Open as a draft</label><button type="button" class="o-btn o-sm o-r" (click)="opening.set(false)">Cancel</button><button type="submit" class="o-btn o-sm o-dark" [disabled]="busy() || !source() || !title.trim()" data-testid="pull-open">Open pull request</button></div>
|
|
2059
|
+
</form>
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
<div class="o-flex o-wrap">
|
|
2063
|
+
<input class="o-in o-sm" style="flex:1;min-width:240px" placeholder="Search titles" aria-label="Search titles" [(ngModel)]="q" (keydown.enter)="load()" (blur)="load()" />
|
|
2064
|
+
<button type="button" class="o-btn o-sm" [class.o-on]="mine()" [attr.aria-pressed]="mine()" (click)="mine.set(!mine()); load()">Needs my review</button>
|
|
2065
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Author" [(ngModel)]="author" (ngModelChange)="load()"><option value="">Author</option>@for (p of host.people(); track p.id) {<option [value]="p.id">{{ p.name || p.email }}</option>}</select>
|
|
2066
|
+
</div>
|
|
2067
|
+
<div class="o-card">
|
|
2068
|
+
<div class="o-ch"><div class="o-flex" style="gap:14px" role="group" aria-label="State">
|
|
2069
|
+
@for (s of states; track s.id) {<button type="button" class="st" [class.o-on]="state() === s.id" [attr.aria-pressed]="state() === s.id" (click)="state.set(s.id); load()">{{ s.icon }} {{ s.label }}</button>}
|
|
2070
|
+
</div></div>
|
|
2071
|
+
@for (p of rows(); track p.repo + p.number) {
|
|
2072
|
+
<a class="o-row o-click" style="align-items:flex-start" [routerLink]="host.link('code/' + p.repo + '/pull/' + p.number)" [attr.data-testid]="'pull-' + p.number">
|
|
2073
|
+
<span [style.color]="p.state === 'merged' ? 'var(--o-vio)' : p.state === 'closed' ? 'var(--o-bad-solid)' : 'var(--o-ok)'" style="margin-top:1px" aria-hidden="true">{{ p.state === 'merged' ? '⇄' : p.state === 'closed' ? '✕' : '●' }}</span>
|
|
2074
|
+
<div class="o-col" style="gap:4px;flex:1;min-width:0">
|
|
2075
|
+
<span class="o-flex o-wrap"><b class="o-b5" style="color:var(--o-tx)">{{ p.title }}</b>@for (l of p.labels; track l) {<span class="o-chip o-out">{{ l }}</span>}@if (p.draft) {<span class="o-chip">Draft</span>}</span>
|
|
2076
|
+
<span class="o-faint o-sm">#{{ p.number }} · {{ people().name(p.authorId) }} · @if (!repoName()) {{{ p.repo }} · }<span class="o-mono">{{ p.source }}</span> → <span class="o-mono">{{ p.target }}</span> · updated {{ ago(p.updatedAt) }}</span>
|
|
2077
|
+
</div>
|
|
2078
|
+
<span class="o-flex o-sm"><span [class]="dot(p.checks)" [attr.title]="'Checks: ' + (p.checks || 'none')"></span><span class="o-mut">{{ review(p.review) }}</span><span class="o-faint" [attr.aria-label]="p.comments + ' comments'">💬 {{ p.comments }}</span></span>
|
|
2079
|
+
</a>
|
|
2080
|
+
} @empty {
|
|
2081
|
+
<div class="o-empty">{{ loading() ? 'Loading…' : repos().length ? 'No pull requests match.' : 'No repositories yet.' }}</div>
|
|
2082
|
+
}
|
|
2083
|
+
</div>
|
|
2084
|
+
</div>
|
|
2085
|
+
`, isInline: true, styles: [".st{border:none;background:none;padding:0;font-size:12px;font-weight:500;cursor:pointer;color:var(--o-mut)}.st.o-on{color:var(--o-tx)}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2086
|
+
}
|
|
2087
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullsPage, decorators: [{
|
|
2088
|
+
type: Component,
|
|
2089
|
+
args: [{ selector: 'o-code-pulls', imports: [FormsModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2090
|
+
<div class="o-page">
|
|
2091
|
+
<div class="o-ph">
|
|
2092
|
+
<div class="o-col"><span class="o-eyebrow">{{ repoName() || host.project()?.key + ' · every repository' }}</span><h1 class="o-t">Pull requests</h1></div>
|
|
2093
|
+
@if (canWrite() && repos().length) {<div class="o-acts"><button type="button" class="o-btn o-dark" (click)="startNew('')" data-testid="new-pull">New pull request</button></div>}
|
|
2094
|
+
</div>
|
|
2095
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2096
|
+
|
|
2097
|
+
@if (opening()) {
|
|
2098
|
+
<form class="o-card" (ngSubmit)="open()" aria-label="New pull request">
|
|
2099
|
+
<div class="o-ch"><b>New pull request</b></div>
|
|
2100
|
+
<div class="o-cb o-col" style="gap:12px">
|
|
2101
|
+
<div class="o-flex o-wrap">
|
|
2102
|
+
@if (!repoName()) {
|
|
2103
|
+
<select class="o-in o-sm" style="width:auto" name="repo" aria-label="Repository" [ngModel]="newRepo()" (ngModelChange)="pickRepo($event)">@for (r of repos(); track r.name) {<option [value]="r.name">{{ r.name }}</option>}</select>
|
|
2104
|
+
}
|
|
2105
|
+
<span class="o-faint o-sm">into</span>
|
|
2106
|
+
<select class="o-in o-sm o-mono" style="width:auto" name="target" aria-label="Target branch" [ngModel]="target()" (ngModelChange)="target.set($event); compare()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}</select>
|
|
2107
|
+
<span class="o-faint o-sm">from</span>
|
|
2108
|
+
<select class="o-in o-sm o-mono" style="width:auto" name="source" aria-label="Source branch" [ngModel]="source()" (ngModelChange)="source.set($event); compare()" data-testid="pull-source">
|
|
2109
|
+
<option value="">Choose a branch</option>
|
|
2110
|
+
@for (b of branches(); track b.name) {@if (b.name !== target()) {<option [value]="b.name">{{ b.name }}</option>}}
|
|
2111
|
+
</select>
|
|
2112
|
+
@if (cmp(); as c) {<span class="o-faint o-sm">{{ c.ahead }} {{ c.ahead === 1 ? 'commit' : 'commits' }} · {{ c.files.length }} {{ c.files.length === 1 ? 'file' : 'files' }}</span>}
|
|
2113
|
+
</div>
|
|
2114
|
+
<div class="o-fld"><label for="np-title">Title</label><input id="np-title" class="o-in" name="title" [(ngModel)]="title" data-testid="pull-title" /><span class="o-hint">Put an issue key in the title or description to link it; “Closes PAY-142” closes it on merge.</span></div>
|
|
2115
|
+
<div class="o-fld"><label for="np-body">Description</label><textarea id="np-body" class="o-in" rows="5" name="body" [(ngModel)]="body" style="height:auto;padding:8px 10px"></textarea></div>
|
|
2116
|
+
<div class="o-fld"><label for="np-rev">Reviewers</label>
|
|
2117
|
+
<select id="np-rev" class="o-in" name="reviewer" [(ngModel)]="reviewer"><option value="">Nobody yet</option>@for (p of host.people(); track p.id) {@if (p.id !== host.meId()) {<option [value]="p.id">{{ p.name || p.email }}</option>}}</select>
|
|
2118
|
+
</div>
|
|
2119
|
+
</div>
|
|
2120
|
+
<div class="o-cf"><label class="o-flex o-sm"><input type="checkbox" name="draft" [(ngModel)]="draft" />Open as a draft</label><button type="button" class="o-btn o-sm o-r" (click)="opening.set(false)">Cancel</button><button type="submit" class="o-btn o-sm o-dark" [disabled]="busy() || !source() || !title.trim()" data-testid="pull-open">Open pull request</button></div>
|
|
2121
|
+
</form>
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
<div class="o-flex o-wrap">
|
|
2125
|
+
<input class="o-in o-sm" style="flex:1;min-width:240px" placeholder="Search titles" aria-label="Search titles" [(ngModel)]="q" (keydown.enter)="load()" (blur)="load()" />
|
|
2126
|
+
<button type="button" class="o-btn o-sm" [class.o-on]="mine()" [attr.aria-pressed]="mine()" (click)="mine.set(!mine()); load()">Needs my review</button>
|
|
2127
|
+
<select class="o-in o-sm" style="width:auto" aria-label="Author" [(ngModel)]="author" (ngModelChange)="load()"><option value="">Author</option>@for (p of host.people(); track p.id) {<option [value]="p.id">{{ p.name || p.email }}</option>}</select>
|
|
2128
|
+
</div>
|
|
2129
|
+
<div class="o-card">
|
|
2130
|
+
<div class="o-ch"><div class="o-flex" style="gap:14px" role="group" aria-label="State">
|
|
2131
|
+
@for (s of states; track s.id) {<button type="button" class="st" [class.o-on]="state() === s.id" [attr.aria-pressed]="state() === s.id" (click)="state.set(s.id); load()">{{ s.icon }} {{ s.label }}</button>}
|
|
2132
|
+
</div></div>
|
|
2133
|
+
@for (p of rows(); track p.repo + p.number) {
|
|
2134
|
+
<a class="o-row o-click" style="align-items:flex-start" [routerLink]="host.link('code/' + p.repo + '/pull/' + p.number)" [attr.data-testid]="'pull-' + p.number">
|
|
2135
|
+
<span [style.color]="p.state === 'merged' ? 'var(--o-vio)' : p.state === 'closed' ? 'var(--o-bad-solid)' : 'var(--o-ok)'" style="margin-top:1px" aria-hidden="true">{{ p.state === 'merged' ? '⇄' : p.state === 'closed' ? '✕' : '●' }}</span>
|
|
2136
|
+
<div class="o-col" style="gap:4px;flex:1;min-width:0">
|
|
2137
|
+
<span class="o-flex o-wrap"><b class="o-b5" style="color:var(--o-tx)">{{ p.title }}</b>@for (l of p.labels; track l) {<span class="o-chip o-out">{{ l }}</span>}@if (p.draft) {<span class="o-chip">Draft</span>}</span>
|
|
2138
|
+
<span class="o-faint o-sm">#{{ p.number }} · {{ people().name(p.authorId) }} · @if (!repoName()) {{{ p.repo }} · }<span class="o-mono">{{ p.source }}</span> → <span class="o-mono">{{ p.target }}</span> · updated {{ ago(p.updatedAt) }}</span>
|
|
2139
|
+
</div>
|
|
2140
|
+
<span class="o-flex o-sm"><span [class]="dot(p.checks)" [attr.title]="'Checks: ' + (p.checks || 'none')"></span><span class="o-mut">{{ review(p.review) }}</span><span class="o-faint" [attr.aria-label]="p.comments + ' comments'">💬 {{ p.comments }}</span></span>
|
|
2141
|
+
</a>
|
|
2142
|
+
} @empty {
|
|
2143
|
+
<div class="o-empty">{{ loading() ? 'Loading…' : repos().length ? 'No pull requests match.' : 'No repositories yet.' }}</div>
|
|
2144
|
+
}
|
|
2145
|
+
</div>
|
|
2146
|
+
</div>
|
|
2147
|
+
`, styles: [".st{border:none;background:none;padding:0;font-size:12px;font-weight:500;cursor:pointer;color:var(--o-mut)}.st.o-on{color:var(--o-tx)}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"] }]
|
|
2148
|
+
}], ctorParameters: () => [] });
|
|
2149
|
+
|
|
2150
|
+
const METHOD = {
|
|
2151
|
+
merge: { label: 'Create a merge commit', hint: 'All commits plus a merge commit', done: 'merged' },
|
|
2152
|
+
squash: { label: 'Squash and merge', hint: 'One commit on the target', done: 'squashed and merged' },
|
|
2153
|
+
rebase: { label: 'Rebase and merge', hint: 'The commits, linear history', done: 'rebased and merged' },
|
|
2154
|
+
};
|
|
2155
|
+
// /{org}/{project}/code/{repo}/pull/{n} (prototype "code"): the pull request's conversation, commits,
|
|
2156
|
+
// files changed with line threads, and checks; the merge box with the gate's rows, the merge method
|
|
2157
|
+
// and branch deletion; a review (comment, approve, request changes).
|
|
2158
|
+
class PullPage extends RepoPage {
|
|
2159
|
+
constructor() {
|
|
2160
|
+
super();
|
|
2161
|
+
this.el = inject(ElementRef);
|
|
2162
|
+
this.pr = signal(null, ...(ngDevMode ? [{ debugName: "pr" }] : /* istanbul ignore next */ []));
|
|
2163
|
+
this.timeline = signal({ comments: [], reviews: [], threads: [] }, ...(ngDevMode ? [{ debugName: "timeline" }] : /* istanbul ignore next */ []));
|
|
2164
|
+
this.cmp = signal(null, ...(ngDevMode ? [{ debugName: "cmp" }] : /* istanbul ignore next */ []));
|
|
2165
|
+
this.tab = signal('conversation', ...(ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []));
|
|
2166
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
2167
|
+
this.draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
2168
|
+
this.reviewOpen = signal(false, ...(ngDevMode ? [{ debugName: "reviewOpen" }] : /* istanbul ignore next */ []));
|
|
2169
|
+
this.revBody = signal('', ...(ngDevMode ? [{ debugName: "revBody" }] : /* istanbul ignore next */ []));
|
|
2170
|
+
this.revKind = signal('commented', ...(ngDevMode ? [{ debugName: "revKind" }] : /* istanbul ignore next */ []));
|
|
2171
|
+
this.mergeMenu = signal(false, ...(ngDevMode ? [{ debugName: "mergeMenu" }] : /* istanbul ignore next */ []));
|
|
2172
|
+
this.method = signal('merge', ...(ngDevMode ? [{ debugName: "method" }] : /* istanbul ignore next */ []));
|
|
2173
|
+
this.delBranch = signal(false, ...(ngDevMode ? [{ debugName: "delBranch" }] : /* istanbul ignore next */ []));
|
|
2174
|
+
this.number = 0;
|
|
2175
|
+
this.ago = (s) => ago(s);
|
|
2176
|
+
this.initials = initials;
|
|
2177
|
+
this.dot = checkDot;
|
|
2178
|
+
this.threads = computed(() => this.timeline().threads, ...(ngDevMode ? [{ debugName: "threads" }] : /* istanbul ignore next */ []));
|
|
2179
|
+
this.openThreads = computed(() => this.threads().filter((t) => !t.resolved && !t.outdated).length, ...(ngDevMode ? [{ debugName: "openThreads" }] : /* istanbul ignore next */ []));
|
|
2180
|
+
this.tabs = computed(() => {
|
|
2181
|
+
const p = this.pr();
|
|
2182
|
+
return [
|
|
2183
|
+
{ id: 'conversation', label: 'Conversation', n: undefined },
|
|
2184
|
+
{ id: 'commits', label: 'Commits', n: p?.commits },
|
|
2185
|
+
{ id: 'files', label: 'Files changed', n: this.cmp()?.files.length },
|
|
2186
|
+
{ id: 'checks', label: 'Checks', n: p?.checks.length },
|
|
2187
|
+
];
|
|
2188
|
+
}, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
|
|
2189
|
+
this.badge = computed(() => {
|
|
2190
|
+
const p = this.pr();
|
|
2191
|
+
return !p ? '' : p.state === 'merged' ? 'Merged' : p.state === 'closed' ? 'Closed' : p.draft ? 'Draft' : 'Open';
|
|
2192
|
+
}, ...(ngDevMode ? [{ debugName: "badge" }] : /* istanbul ignore next */ []));
|
|
2193
|
+
this.badgeBg = computed(() => ({ Merged: 'var(--o-vio)', Closed: 'var(--o-bad-solid)', Draft: 'var(--o-dim)', Open: 'var(--o-ok)' }[this.badge()] ?? 'var(--o-dim)'), ...(ngDevMode ? [{ debugName: "badgeBg" }] : /* istanbul ignore next */ []));
|
|
2194
|
+
this.approvedByMe = computed(() => (this.pr()?.reviews ?? []).some((r) => r.authorId === this.host.meId() && r.state === 'approved' && !r.stale), ...(ngDevMode ? [{ debugName: "approvedByMe" }] : /* istanbul ignore next */ []));
|
|
2195
|
+
this.reviewKinds = computed(() => {
|
|
2196
|
+
const need = this.pr()?.gate.rows.find((r) => /required by branch protection|requires/.test(r.text + (r.meta ?? '')));
|
|
2197
|
+
return [
|
|
2198
|
+
{ id: 'commented', label: 'Comment', hint: 'General feedback without approving.' },
|
|
2199
|
+
{ id: 'approved', label: 'Approve', hint: need ? `Counts toward the approvals ${this.pr()?.target} requires.` : 'Approve these changes.' },
|
|
2200
|
+
{ id: 'changes_requested', label: 'Request changes', hint: 'Blocks merge until you approve.' },
|
|
2201
|
+
];
|
|
2202
|
+
}, ...(ngDevMode ? [{ debugName: "reviewKinds" }] : /* istanbul ignore next */ []));
|
|
2203
|
+
this.reviewers = computed(() => {
|
|
2204
|
+
const p = this.pr();
|
|
2205
|
+
if (!p)
|
|
2206
|
+
return [];
|
|
2207
|
+
const ids = [...new Set([...p.reviewers, ...p.reviews.map((r) => r.authorId)])];
|
|
2208
|
+
return ids.map((id) => {
|
|
2209
|
+
const r = p.reviews.find((x) => x.authorId === id);
|
|
2210
|
+
const state = r ? (r.stale ? 'stale' : r.state) : 'requested';
|
|
2211
|
+
const label = { approved: 'Approved', changes_requested: 'Changes requested', commented: 'Commented', dismissed: 'Dismissed', stale: 'Approved an older commit', requested: 'Requested' }[state] ?? state;
|
|
2212
|
+
const colour = state === 'approved' ? 'var(--o-ok-tx)' : state === 'changes_requested' ? 'var(--o-bad-tx)' : 'var(--o-dim)';
|
|
2213
|
+
return { id, label, colour };
|
|
2214
|
+
});
|
|
2215
|
+
}, ...(ngDevMode ? [{ debugName: "reviewers" }] : /* istanbul ignore next */ []));
|
|
2216
|
+
this.events = computed(() => {
|
|
2217
|
+
const t = this.timeline();
|
|
2218
|
+
const out = [];
|
|
2219
|
+
for (const c of t.comments)
|
|
2220
|
+
if (!c.threadId)
|
|
2221
|
+
out.push({ id: 'c' + c.id, at: c.createdAt, kind: 'comment', who: c.authorId, body: c.body + (c.pending ? ' (pending)' : '') });
|
|
2222
|
+
for (const r of t.reviews)
|
|
2223
|
+
if (r.state !== 'pending')
|
|
2224
|
+
out.push({ id: 'r' + r.id, at: r.submittedAt ?? r.createdAt, kind: 'review', who: r.authorId, state: r.state, body: r.body });
|
|
2225
|
+
for (const th of t.threads)
|
|
2226
|
+
out.push({ id: 't' + th.id, at: th.createdAt, kind: 'thread', who: th.createdBy, thread: th });
|
|
2227
|
+
return out.sort((a, b) => a.at.localeCompare(b.at));
|
|
2228
|
+
}, ...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
2229
|
+
this.route.paramMap.subscribe((m) => {
|
|
2230
|
+
this.number = Number(m.get('number') ?? 0);
|
|
2231
|
+
const r = this.repo();
|
|
2232
|
+
if (r && r.name === this.name())
|
|
2233
|
+
this.onRepo(r);
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
onRepo(_) {
|
|
2237
|
+
if (this.number)
|
|
2238
|
+
this.load(true);
|
|
2239
|
+
}
|
|
2240
|
+
md(s) {
|
|
2241
|
+
return marked.parse(s || '', { async: false, gfm: true, breaks: true });
|
|
2242
|
+
}
|
|
2243
|
+
pick(t) {
|
|
2244
|
+
this.tab.set(t);
|
|
2245
|
+
}
|
|
2246
|
+
methodLabel(m) {
|
|
2247
|
+
return METHOD[m]?.label.replace('Create a merge commit', 'Merge pull request') ?? m;
|
|
2248
|
+
}
|
|
2249
|
+
methodHint(m) {
|
|
2250
|
+
return METHOD[m]?.hint ?? '';
|
|
2251
|
+
}
|
|
2252
|
+
methodDone(m) {
|
|
2253
|
+
return METHOD[m ?? 'merge']?.done ?? 'merged';
|
|
2254
|
+
}
|
|
2255
|
+
reviewVerb(state) {
|
|
2256
|
+
return state === 'approved' ? 'approved these changes' : state === 'changes_requested' ? 'requested changes' : state === 'dismissed' ? 'had a review dismissed' : 'reviewed';
|
|
2257
|
+
}
|
|
2258
|
+
rowDot(status) {
|
|
2259
|
+
return status === 'ok' ? 'o-dot o-grn' : status === 'failed' ? 'o-dot o-red' : 'o-dot o-amb';
|
|
2260
|
+
}
|
|
2261
|
+
approve() {
|
|
2262
|
+
this.sendReview('approved', '');
|
|
2263
|
+
}
|
|
2264
|
+
submitReview() {
|
|
2265
|
+
this.sendReview(this.revKind(), this.revBody().trim());
|
|
2266
|
+
}
|
|
2267
|
+
addComment() {
|
|
2268
|
+
const body = this.draft().trim();
|
|
2269
|
+
if (!body)
|
|
2270
|
+
return;
|
|
2271
|
+
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body }), 'Could not add the comment.', () => this.draft.set(''));
|
|
2272
|
+
}
|
|
2273
|
+
lineComment(c) {
|
|
2274
|
+
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body: c.body, path: c.path, side: c.side, line: c.line }), 'Could not add the comment.');
|
|
2275
|
+
}
|
|
2276
|
+
reply(r) {
|
|
2277
|
+
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body: r.body, threadId: r.threadId }), 'Could not reply.');
|
|
2278
|
+
}
|
|
2279
|
+
resolve(e) {
|
|
2280
|
+
this.run(this.api.resolve(this.projectId(), this.name(), this.number, e.threadId, e.resolved), 'Could not change the conversation.');
|
|
2281
|
+
}
|
|
2282
|
+
merge() {
|
|
2283
|
+
const p = this.pr();
|
|
2284
|
+
if (!p)
|
|
2285
|
+
return;
|
|
2286
|
+
this.run(this.api.merge(this.projectId(), this.name(), this.number, { method: this.method(), deleteBranch: this.delBranch(), expectHead: p.headSha }), 'Could not merge.', () => this.note.set(`Merged into ${p.target}.`));
|
|
2287
|
+
}
|
|
2288
|
+
close(reopen) {
|
|
2289
|
+
this.run(this.api.closePull(this.projectId(), this.name(), this.number, reopen), reopen ? 'Could not reopen.' : 'Could not close.');
|
|
2290
|
+
}
|
|
2291
|
+
outside(e) {
|
|
2292
|
+
const t = e.target;
|
|
2293
|
+
if (this.reviewOpen() && !t.closest('.anchor'))
|
|
2294
|
+
this.reviewOpen.set(false);
|
|
2295
|
+
if (this.mergeMenu() && !t.closest('.merge'))
|
|
2296
|
+
this.mergeMenu.set(false);
|
|
2297
|
+
}
|
|
2298
|
+
sendReview(state, body) {
|
|
2299
|
+
this.run(this.api.review(this.projectId(), this.name(), this.number, state, body), 'Could not submit the review.', () => { this.reviewOpen.set(false); this.revBody.set(''); });
|
|
2300
|
+
}
|
|
2301
|
+
run(o, fallback, after) {
|
|
2302
|
+
this.busy.set(true);
|
|
2303
|
+
this.error.set('');
|
|
2304
|
+
o.subscribe({
|
|
2305
|
+
next: () => { this.busy.set(false); after?.(); this.load(false); },
|
|
2306
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, fallback)); this.load(false); },
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
load(first) {
|
|
2310
|
+
const project = this.projectId();
|
|
2311
|
+
this.api.pull(project, this.name(), this.number).subscribe({
|
|
2312
|
+
next: (p) => {
|
|
2313
|
+
this.pr.set(p);
|
|
2314
|
+
if (first) {
|
|
2315
|
+
this.method.set(p.mergeMethods[0] ?? 'merge');
|
|
2316
|
+
this.delBranch.set(p.deleteBranchOnMerge);
|
|
2317
|
+
}
|
|
2318
|
+
if (first || !this.cmp() || this.cmp().head !== p.headSha) {
|
|
2319
|
+
this.api.compare(project, this.name(), p.mergeBase || p.targetSha, p.headSha).subscribe({ next: (c) => this.cmp.set(c), error: () => this.cmp.set({ base: '', head: p.headSha, mergeBase: '', ahead: 0, behind: 0, commits: [], files: [] }) });
|
|
2320
|
+
}
|
|
2321
|
+
},
|
|
2322
|
+
error: (e) => this.error.set(e?.status === 404 ? `There is no pull request #${this.number} in ${this.name()}.` : apiMessage(e, 'Could not open the pull request.')),
|
|
2323
|
+
});
|
|
2324
|
+
this.api.conversation(project, this.name(), this.number).subscribe({ next: (t) => this.timeline.set(t), error: () => undefined });
|
|
2325
|
+
}
|
|
2326
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2327
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: PullPage, isStandalone: true, selector: "o-code-pull", host: { listeners: { "document:click": "outside($event)" } }, usesInheritance: true, ngImport: i0, template: `
|
|
2328
|
+
<div style="max-width:1180px;margin:0 auto;padding:22px 24px 40px;display:flex;flex-direction:column;gap:16px">
|
|
2329
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2330
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
2331
|
+
@if (pr(); as p) {
|
|
2332
|
+
<div class="o-col">
|
|
2333
|
+
<span class="o-eyebrow">{{ name() }} · pull/{{ p.number }}</span>
|
|
2334
|
+
<h1 class="o-t">{{ p.title }} <span class="o-faint" style="font-weight:400">#{{ p.number }}</span></h1>
|
|
2335
|
+
<div class="o-flex o-wrap o-mut">
|
|
2336
|
+
<span class="badge" [style.background]="badgeBg()" data-testid="pull-state">{{ badge() }}</span>
|
|
2337
|
+
<span>{{ people().name(p.authorId) }} {{ p.state === 'merged' ? 'merged' : 'wants to merge' }} {{ p.commits }} {{ p.commits === 1 ? 'commit' : 'commits' }} into</span>
|
|
2338
|
+
<span class="o-chip o-mono">{{ p.target }}</span><span>from</span><span class="o-chip o-mono">{{ p.source }}</span>
|
|
2339
|
+
</div>
|
|
2340
|
+
</div>
|
|
2341
|
+
<div class="o-tabs" role="tablist">
|
|
2342
|
+
@for (t of tabs(); track t.id) {<button type="button" role="tab" class="o-tab" [class.o-on]="tab() === t.id" [attr.aria-selected]="tab() === t.id" (click)="pick(t.id)">{{ t.label }}@if (t.n !== undefined) {<span class="o-n">{{ t.n }}</span>}</button>}
|
|
2343
|
+
<span class="o-r"></span>
|
|
2344
|
+
@if (p.state === 'open' && p.canReview) {
|
|
2345
|
+
<div class="anchor">
|
|
2346
|
+
<button type="button" class="o-btn o-sm o-pri" (click)="reviewOpen.set(!reviewOpen())" [attr.aria-expanded]="reviewOpen()" aria-haspopup="dialog" data-testid="review-open">Review changes ⌄</button>
|
|
2347
|
+
@if (reviewOpen()) {
|
|
2348
|
+
<div class="o-menu" role="dialog" aria-label="Finish your review" style="right:0;top:36px;width:380px;padding:14px;display:flex;flex-direction:column;gap:10px">
|
|
2349
|
+
<b>Finish your review</b>
|
|
2350
|
+
<textarea class="o-in" rows="3" placeholder="Leave a comment" aria-label="Review comment" style="height:auto;padding:8px 10px" [ngModel]="revBody()" (ngModelChange)="revBody.set($event)"></textarea>
|
|
2351
|
+
@for (k of reviewKinds(); track k.id) {
|
|
2352
|
+
<button type="button" class="o-opt" style="padding:8px 10px" [class.o-on]="revKind() === k.id" (click)="revKind.set(k.id)"><span class="o-rad" [class.o-on]="revKind() === k.id"></span><span class="o-col" style="gap:1px"><b class="o-b5">{{ k.label }}</b><span class="o-faint o-sm">{{ k.hint }}</span></span></button>
|
|
2353
|
+
}
|
|
2354
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm o-pri o-r" (click)="submitReview()" [disabled]="busy() || (revKind() !== 'approved' && !revBody().trim())" data-testid="review-submit">Submit review</button></div>
|
|
2355
|
+
</div>
|
|
2356
|
+
}
|
|
2357
|
+
</div>
|
|
2358
|
+
}
|
|
2359
|
+
</div>
|
|
2360
|
+
|
|
2361
|
+
<div class="o-split" style="grid-template-columns:minmax(0,1fr) 260px;gap:24px">
|
|
2362
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
2363
|
+
@switch (tab()) {
|
|
2364
|
+
@case ('conversation') {
|
|
2365
|
+
<div class="o-card">
|
|
2366
|
+
<div style="padding:9px 14px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft);color:var(--o-mut);font-size:12px"><b style="color:var(--o-tx);font-weight:600">{{ people().name(p.authorId) }}</b> opened this {{ ago(p.createdAt) }}</div>
|
|
2367
|
+
<div class="o-cb o-md" style="font-size:13px" [innerHTML]="p.body ? md(p.body) : '<p class="o-faint">No description.</p>'"></div>
|
|
2368
|
+
</div>
|
|
2369
|
+
<div class="tl">
|
|
2370
|
+
@for (e of events(); track e.id) {
|
|
2371
|
+
@switch (e.kind) {
|
|
2372
|
+
@case ('comment') {
|
|
2373
|
+
<div class="o-card o-cb o-col" style="gap:6px"><span class="o-sm o-mut"><b style="color:var(--o-tx);font-weight:600">{{ people().name(e.who) }}</b> commented {{ ago(e.at) }}</span><div class="o-md" style="font-size:13px" [innerHTML]="md(e.body ?? '')"></div></div>
|
|
2374
|
+
}
|
|
2375
|
+
@case ('review') {
|
|
2376
|
+
<span [style.color]="e.state === 'approved' ? 'var(--o-ok-tx)' : e.state === 'changes_requested' ? 'var(--o-bad-tx)' : 'var(--o-mut)'">{{ people().name(e.who) }} {{ reviewVerb(e.state) }} · {{ ago(e.at) }}@if (e.body) {: “{{ e.body }}”}</span>
|
|
2377
|
+
}
|
|
2378
|
+
@case ('thread') {
|
|
2379
|
+
<div class="o-card o-cb o-col" style="gap:6px">
|
|
2380
|
+
<span class="o-sm o-mut"><b style="color:var(--o-tx);font-weight:600">{{ people().name(e.who) }}</b> commented on <button type="button" class="o-lnk o-mono" (click)="pick('files')">{{ e.thread!.path }}</button> line {{ e.thread!.line }}@if (e.thread!.outdated) { · <span class="o-chip">Outdated</span>}</span>
|
|
2381
|
+
@for (c of e.thread!.comments; track c.id) {<span class="o-sm"><b class="o-b5">{{ people().name(c.authorId) }}</b> {{ c.body }}</span>}
|
|
2382
|
+
<div class="o-flex"><span class="o-faint o-sm">{{ e.thread!.resolved ? 'Resolved' + (e.thread!.resolvedBy ? ' by ' + people().name(e.thread!.resolvedBy) : '') : 'Unresolved' }}</span>@if (p.state === 'open') {<button type="button" class="o-lnk o-r" (click)="resolve({ threadId: e.thread!.id, resolved: !e.thread!.resolved })">{{ e.thread!.resolved ? 'Unresolve' : 'Resolve conversation' }}</button>}</div>
|
|
2383
|
+
</div>
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
@if (p.state === 'merged') {<span style="color:var(--o-vio-tx)" data-testid="merged-line">{{ people().name(p.mergedBy) }} {{ methodDone(p.mergeMethod) }} into {{ p.target }} · {{ ago(p.mergedAt) }}</span>}
|
|
2388
|
+
@if (p.state === 'closed') {<span class="o-red-t">Closed {{ ago(p.closedAt) }}</span>}
|
|
2389
|
+
</div>
|
|
2390
|
+
|
|
2391
|
+
<div class="o-card" [style.border-color]="p.state === 'open' ? (p.gate.mergeable ? 'var(--o-ok)' : 'var(--o-line-mid)') : null" data-testid="merge-box">
|
|
2392
|
+
<div class="o-cb o-col" style="gap:10px;border-bottom:1px solid var(--o-line-soft)">
|
|
2393
|
+
<span style="font-weight:600;font-size:14px" [style.color]="p.state === 'merged' ? 'var(--o-vio-tx)' : p.gate.mergeable ? 'var(--o-ok-tx)' : 'var(--o-tx)'">{{ p.state === 'merged' ? 'Merged' : p.state === 'closed' ? 'Closed without merging' : p.gate.title }}</span>
|
|
2394
|
+
@if (p.state === 'open') {
|
|
2395
|
+
@for (r of p.gate.rows; track $index) {
|
|
2396
|
+
<div class="o-flex"><span [class]="rowDot(r.status)" aria-hidden="true"></span><span>{{ r.text }}</span><span class="o-r o-faint o-sm">{{ r.meta }}</span></div>
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
</div>
|
|
2400
|
+
@if (p.state === 'open') {
|
|
2401
|
+
<div class="o-flex o-wrap" style="padding:12px 14px;background:var(--o-well)">
|
|
2402
|
+
@if (p.canReview && !approvedByMe()) {<button type="button" class="o-btn" (click)="approve()" [disabled]="busy()" data-testid="approve">Approve</button>}
|
|
2403
|
+
@if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
|
|
2404
|
+
@if (p.canMerge) {
|
|
2405
|
+
<span class="o-faint o-sm">Delete branch after merge</span><button type="button" class="o-tog" role="switch" aria-label="Delete branch after merge" [class.o-on]="delBranch()" [attr.aria-checked]="delBranch()" (click)="delBranch.set(!delBranch())"></button>
|
|
2406
|
+
<div class="merge o-r">
|
|
2407
|
+
<button type="button" class="go" (click)="merge()" [disabled]="!p.gate.mergeable || busy()" data-testid="merge">{{ methodLabel(method()) }}</button>
|
|
2408
|
+
<button type="button" class="mn" (click)="mergeMenu.set(!mergeMenu())" [attr.aria-expanded]="mergeMenu()" aria-label="Choose the merge method" [disabled]="busy()">⌄</button>
|
|
2409
|
+
@if (mergeMenu()) {
|
|
2410
|
+
<div class="o-menu" role="menu" style="right:0;bottom:40px;width:300px">
|
|
2411
|
+
@for (m of p.mergeMethods; track m) {<button type="button" role="menuitemradio" [attr.aria-checked]="method() === m" (click)="method.set(m); mergeMenu.set(false)"><span class="o-col" style="gap:1px"><b class="o-b5">{{ methodLabel(m) }}</b><span class="o-faint o-xs">{{ methodHint(m) }}</span></span></button>}
|
|
2412
|
+
</div>
|
|
2413
|
+
}
|
|
2414
|
+
</div>
|
|
2415
|
+
}
|
|
2416
|
+
</div>
|
|
2417
|
+
} @else if (p.state === 'closed' && p.canEdit) {
|
|
2418
|
+
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><button type="button" class="o-btn o-r" (click)="close(true)" [disabled]="busy()">Reopen</button></div>
|
|
2419
|
+
} @else if (p.state === 'merged') {
|
|
2420
|
+
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><span class="o-mut">{{ p.sourceDeleted ? 'Branch ' + p.source + ' deleted.' : 'Branch ' + p.source + ' can be deleted.' }}</span>@if (p.mergedSha) {<a class="o-btn o-r" [routerLink]="link('commit/' + p.mergedSha)">View {{ p.mergedSha.slice(0, 7) }}</a>}</div>
|
|
2421
|
+
}
|
|
2422
|
+
</div>
|
|
2423
|
+
|
|
2424
|
+
<div class="o-card">
|
|
2425
|
+
<label class="o-sr-only" for="pr-cmt">Add a comment</label>
|
|
2426
|
+
<textarea id="pr-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;height:auto;padding:10px 12px" placeholder="Add a comment. Markdown works." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="pull-comment"></textarea>
|
|
2427
|
+
<div class="o-cf"><span>Markdown</span><button type="button" class="o-btn o-sm o-dark o-r" (click)="addComment()" [disabled]="!draft().trim() || busy()" data-testid="pull-comment-add">Comment</button></div>
|
|
2428
|
+
</div>
|
|
2429
|
+
}
|
|
2430
|
+
@case ('commits') {
|
|
2431
|
+
<div class="o-card">
|
|
2432
|
+
@for (c of cmp()?.commits ?? []; track c.oid) {
|
|
2433
|
+
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a><a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
|
|
2434
|
+
} @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
|
|
2435
|
+
</div>
|
|
2436
|
+
}
|
|
2437
|
+
@case ('files') {
|
|
2438
|
+
<div class="o-flex o-wrap"><span class="o-faint o-sm">{{ cmp()?.files?.length ?? 0 }} {{ cmp()?.files?.length === 1 ? 'file' : 'files' }} · click a line number to comment</span><span class="o-r o-faint o-sm">{{ openThreads() }} unresolved {{ openThreads() === 1 ? 'conversation' : 'conversations' }}</span></div>
|
|
2439
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2440
|
+
<o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" (comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" />
|
|
2441
|
+
} @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
|
|
2442
|
+
}
|
|
2443
|
+
@case ('checks') {
|
|
2444
|
+
<div class="o-card">
|
|
2445
|
+
@for (c of p.checks; track c.id) {
|
|
2446
|
+
<div class="o-row"><span [class]="dot(c.state)" aria-hidden="true"></span><span class="o-mono o-sm">{{ c.context }}</span><span class="o-faint">{{ c.description }}</span>@if (c.url) {<a class="o-r o-sm" [href]="c.url" target="_blank" rel="noopener">Details</a>}</div>
|
|
2447
|
+
} @empty {<div class="o-empty">No checks reported on {{ p.headSha.slice(0, 7) }}. Pipelines report here once Deploy is set up.</div>}
|
|
2448
|
+
</div>
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
</div>
|
|
2452
|
+
|
|
2453
|
+
<div class="o-col" style="gap:18px;font-size:13px">
|
|
2454
|
+
<div class="o-col">
|
|
2455
|
+
<span class="o-lbl">Reviewers</span>
|
|
2456
|
+
@for (r of reviewers(); track r.id) {<div class="o-flex"><span class="o-av" aria-hidden="true">{{ people().initials(r.id) }}</span><span>{{ people().name(r.id) }}</span><span class="o-r o-sm" [style.color]="r.colour">{{ r.label }}</span></div>}
|
|
2457
|
+
@if (!reviewers().length) {<span class="o-faint o-sm">No reviewers yet.</span>}
|
|
2458
|
+
<span class="o-faint o-xs">CODEOWNERS requests owners of changed paths at GA.</span>
|
|
2459
|
+
</div>
|
|
2460
|
+
<div class="o-col"><span class="o-lbl">Labels</span><div class="o-flex o-wrap">@for (l of p.labels; track l) {<span class="o-chip o-out">{{ l }}</span>}@if (!p.labels.length) {<span class="o-faint o-sm">None</span>}</div></div>
|
|
2461
|
+
<div class="o-col">
|
|
2462
|
+
<span class="o-lbl">Linked issues</span>
|
|
2463
|
+
@for (k of p.keys; track k) {<a class="o-card o-cb o-click o-col" style="gap:4px;padding:10px 12px;color:var(--o-tx)" [routerLink]="host.link('issues/' + k)"><span class="o-mono o-xs o-mut">{{ k }}</span></a>}
|
|
2464
|
+
@if (!p.keys.length) {<span class="o-faint o-sm">Name an issue key in the title or description to link it.</span>}
|
|
2465
|
+
</div>
|
|
2466
|
+
<div class="o-col"><span class="o-lbl">Deployments</span><span class="o-faint o-sm">Releases that contain this pull request appear here after merge.</span></div>
|
|
2467
|
+
@if (canProtect()) {<div class="o-col"><span class="o-lbl">Branch protection · {{ p.target }}</span><a class="o-sm" [routerLink]="link('settings')" [queryParams]="{ section: 'protect' }">Edit rules</a></div>}
|
|
2468
|
+
</div>
|
|
2469
|
+
</div>
|
|
2470
|
+
} @else if (!error()) {
|
|
2471
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
2472
|
+
}
|
|
2473
|
+
</div>
|
|
2474
|
+
`, isInline: true, styles: [".badge{padding:3px 10px;border-radius:12px;font-size:12px;font-weight:500;color:var(--o-acc-fg)}.anchor{position:relative;align-self:center}.merge{display:flex;align-items:center;position:relative}.merge .go{height:32px;padding:0 14px;border-radius:7px 0 0 7px;border:none;font-size:13px;font-weight:500;color:var(--o-acc-fg);background:var(--o-ok);cursor:pointer}.merge .mn{height:32px;padding:0 10px;border:none;border-radius:0 7px 7px 0;font-size:12px;color:var(--o-acc-fg);background:var(--o-ok);border-left:1px solid rgba(255,255,255,.25);cursor:pointer}.merge button:disabled{background:var(--o-line-mid);cursor:not-allowed}.tl{display:flex;flex-direction:column;gap:10px;padding-left:14px;border-left:2px solid var(--o-line);margin-left:14px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: DiffView, selector: "o-diff", inputs: ["repo", "file", "head", "base", "threads", "canComment"], outputs: ["comment", "reply", "resolve"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2475
|
+
}
|
|
2476
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullPage, decorators: [{
|
|
2477
|
+
type: Component,
|
|
2478
|
+
args: [{ selector: 'o-code-pull', imports: [FormsModule, RouterLink, DiffView], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2479
|
+
<div style="max-width:1180px;margin:0 auto;padding:22px 24px 40px;display:flex;flex-direction:column;gap:16px">
|
|
2480
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2481
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
2482
|
+
@if (pr(); as p) {
|
|
2483
|
+
<div class="o-col">
|
|
2484
|
+
<span class="o-eyebrow">{{ name() }} · pull/{{ p.number }}</span>
|
|
2485
|
+
<h1 class="o-t">{{ p.title }} <span class="o-faint" style="font-weight:400">#{{ p.number }}</span></h1>
|
|
2486
|
+
<div class="o-flex o-wrap o-mut">
|
|
2487
|
+
<span class="badge" [style.background]="badgeBg()" data-testid="pull-state">{{ badge() }}</span>
|
|
2488
|
+
<span>{{ people().name(p.authorId) }} {{ p.state === 'merged' ? 'merged' : 'wants to merge' }} {{ p.commits }} {{ p.commits === 1 ? 'commit' : 'commits' }} into</span>
|
|
2489
|
+
<span class="o-chip o-mono">{{ p.target }}</span><span>from</span><span class="o-chip o-mono">{{ p.source }}</span>
|
|
2490
|
+
</div>
|
|
2491
|
+
</div>
|
|
2492
|
+
<div class="o-tabs" role="tablist">
|
|
2493
|
+
@for (t of tabs(); track t.id) {<button type="button" role="tab" class="o-tab" [class.o-on]="tab() === t.id" [attr.aria-selected]="tab() === t.id" (click)="pick(t.id)">{{ t.label }}@if (t.n !== undefined) {<span class="o-n">{{ t.n }}</span>}</button>}
|
|
2494
|
+
<span class="o-r"></span>
|
|
2495
|
+
@if (p.state === 'open' && p.canReview) {
|
|
2496
|
+
<div class="anchor">
|
|
2497
|
+
<button type="button" class="o-btn o-sm o-pri" (click)="reviewOpen.set(!reviewOpen())" [attr.aria-expanded]="reviewOpen()" aria-haspopup="dialog" data-testid="review-open">Review changes ⌄</button>
|
|
2498
|
+
@if (reviewOpen()) {
|
|
2499
|
+
<div class="o-menu" role="dialog" aria-label="Finish your review" style="right:0;top:36px;width:380px;padding:14px;display:flex;flex-direction:column;gap:10px">
|
|
2500
|
+
<b>Finish your review</b>
|
|
2501
|
+
<textarea class="o-in" rows="3" placeholder="Leave a comment" aria-label="Review comment" style="height:auto;padding:8px 10px" [ngModel]="revBody()" (ngModelChange)="revBody.set($event)"></textarea>
|
|
2502
|
+
@for (k of reviewKinds(); track k.id) {
|
|
2503
|
+
<button type="button" class="o-opt" style="padding:8px 10px" [class.o-on]="revKind() === k.id" (click)="revKind.set(k.id)"><span class="o-rad" [class.o-on]="revKind() === k.id"></span><span class="o-col" style="gap:1px"><b class="o-b5">{{ k.label }}</b><span class="o-faint o-sm">{{ k.hint }}</span></span></button>
|
|
2504
|
+
}
|
|
2505
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm o-pri o-r" (click)="submitReview()" [disabled]="busy() || (revKind() !== 'approved' && !revBody().trim())" data-testid="review-submit">Submit review</button></div>
|
|
2506
|
+
</div>
|
|
2507
|
+
}
|
|
2508
|
+
</div>
|
|
2509
|
+
}
|
|
2510
|
+
</div>
|
|
2511
|
+
|
|
2512
|
+
<div class="o-split" style="grid-template-columns:minmax(0,1fr) 260px;gap:24px">
|
|
2513
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
2514
|
+
@switch (tab()) {
|
|
2515
|
+
@case ('conversation') {
|
|
2516
|
+
<div class="o-card">
|
|
2517
|
+
<div style="padding:9px 14px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft);color:var(--o-mut);font-size:12px"><b style="color:var(--o-tx);font-weight:600">{{ people().name(p.authorId) }}</b> opened this {{ ago(p.createdAt) }}</div>
|
|
2518
|
+
<div class="o-cb o-md" style="font-size:13px" [innerHTML]="p.body ? md(p.body) : '<p class="o-faint">No description.</p>'"></div>
|
|
2519
|
+
</div>
|
|
2520
|
+
<div class="tl">
|
|
2521
|
+
@for (e of events(); track e.id) {
|
|
2522
|
+
@switch (e.kind) {
|
|
2523
|
+
@case ('comment') {
|
|
2524
|
+
<div class="o-card o-cb o-col" style="gap:6px"><span class="o-sm o-mut"><b style="color:var(--o-tx);font-weight:600">{{ people().name(e.who) }}</b> commented {{ ago(e.at) }}</span><div class="o-md" style="font-size:13px" [innerHTML]="md(e.body ?? '')"></div></div>
|
|
2525
|
+
}
|
|
2526
|
+
@case ('review') {
|
|
2527
|
+
<span [style.color]="e.state === 'approved' ? 'var(--o-ok-tx)' : e.state === 'changes_requested' ? 'var(--o-bad-tx)' : 'var(--o-mut)'">{{ people().name(e.who) }} {{ reviewVerb(e.state) }} · {{ ago(e.at) }}@if (e.body) {: “{{ e.body }}”}</span>
|
|
2528
|
+
}
|
|
2529
|
+
@case ('thread') {
|
|
2530
|
+
<div class="o-card o-cb o-col" style="gap:6px">
|
|
2531
|
+
<span class="o-sm o-mut"><b style="color:var(--o-tx);font-weight:600">{{ people().name(e.who) }}</b> commented on <button type="button" class="o-lnk o-mono" (click)="pick('files')">{{ e.thread!.path }}</button> line {{ e.thread!.line }}@if (e.thread!.outdated) { · <span class="o-chip">Outdated</span>}</span>
|
|
2532
|
+
@for (c of e.thread!.comments; track c.id) {<span class="o-sm"><b class="o-b5">{{ people().name(c.authorId) }}</b> {{ c.body }}</span>}
|
|
2533
|
+
<div class="o-flex"><span class="o-faint o-sm">{{ e.thread!.resolved ? 'Resolved' + (e.thread!.resolvedBy ? ' by ' + people().name(e.thread!.resolvedBy) : '') : 'Unresolved' }}</span>@if (p.state === 'open') {<button type="button" class="o-lnk o-r" (click)="resolve({ threadId: e.thread!.id, resolved: !e.thread!.resolved })">{{ e.thread!.resolved ? 'Unresolve' : 'Resolve conversation' }}</button>}</div>
|
|
2534
|
+
</div>
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
@if (p.state === 'merged') {<span style="color:var(--o-vio-tx)" data-testid="merged-line">{{ people().name(p.mergedBy) }} {{ methodDone(p.mergeMethod) }} into {{ p.target }} · {{ ago(p.mergedAt) }}</span>}
|
|
2539
|
+
@if (p.state === 'closed') {<span class="o-red-t">Closed {{ ago(p.closedAt) }}</span>}
|
|
2540
|
+
</div>
|
|
2541
|
+
|
|
2542
|
+
<div class="o-card" [style.border-color]="p.state === 'open' ? (p.gate.mergeable ? 'var(--o-ok)' : 'var(--o-line-mid)') : null" data-testid="merge-box">
|
|
2543
|
+
<div class="o-cb o-col" style="gap:10px;border-bottom:1px solid var(--o-line-soft)">
|
|
2544
|
+
<span style="font-weight:600;font-size:14px" [style.color]="p.state === 'merged' ? 'var(--o-vio-tx)' : p.gate.mergeable ? 'var(--o-ok-tx)' : 'var(--o-tx)'">{{ p.state === 'merged' ? 'Merged' : p.state === 'closed' ? 'Closed without merging' : p.gate.title }}</span>
|
|
2545
|
+
@if (p.state === 'open') {
|
|
2546
|
+
@for (r of p.gate.rows; track $index) {
|
|
2547
|
+
<div class="o-flex"><span [class]="rowDot(r.status)" aria-hidden="true"></span><span>{{ r.text }}</span><span class="o-r o-faint o-sm">{{ r.meta }}</span></div>
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
</div>
|
|
2551
|
+
@if (p.state === 'open') {
|
|
2552
|
+
<div class="o-flex o-wrap" style="padding:12px 14px;background:var(--o-well)">
|
|
2553
|
+
@if (p.canReview && !approvedByMe()) {<button type="button" class="o-btn" (click)="approve()" [disabled]="busy()" data-testid="approve">Approve</button>}
|
|
2554
|
+
@if (p.canEdit) {<button type="button" class="o-btn o-ghost" (click)="close(false)" [disabled]="busy()">Close</button>}
|
|
2555
|
+
@if (p.canMerge) {
|
|
2556
|
+
<span class="o-faint o-sm">Delete branch after merge</span><button type="button" class="o-tog" role="switch" aria-label="Delete branch after merge" [class.o-on]="delBranch()" [attr.aria-checked]="delBranch()" (click)="delBranch.set(!delBranch())"></button>
|
|
2557
|
+
<div class="merge o-r">
|
|
2558
|
+
<button type="button" class="go" (click)="merge()" [disabled]="!p.gate.mergeable || busy()" data-testid="merge">{{ methodLabel(method()) }}</button>
|
|
2559
|
+
<button type="button" class="mn" (click)="mergeMenu.set(!mergeMenu())" [attr.aria-expanded]="mergeMenu()" aria-label="Choose the merge method" [disabled]="busy()">⌄</button>
|
|
2560
|
+
@if (mergeMenu()) {
|
|
2561
|
+
<div class="o-menu" role="menu" style="right:0;bottom:40px;width:300px">
|
|
2562
|
+
@for (m of p.mergeMethods; track m) {<button type="button" role="menuitemradio" [attr.aria-checked]="method() === m" (click)="method.set(m); mergeMenu.set(false)"><span class="o-col" style="gap:1px"><b class="o-b5">{{ methodLabel(m) }}</b><span class="o-faint o-xs">{{ methodHint(m) }}</span></span></button>}
|
|
2563
|
+
</div>
|
|
2564
|
+
}
|
|
2565
|
+
</div>
|
|
2566
|
+
}
|
|
2567
|
+
</div>
|
|
2568
|
+
} @else if (p.state === 'closed' && p.canEdit) {
|
|
2569
|
+
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><button type="button" class="o-btn o-r" (click)="close(true)" [disabled]="busy()">Reopen</button></div>
|
|
2570
|
+
} @else if (p.state === 'merged') {
|
|
2571
|
+
<div class="o-flex" style="padding:12px 14px;background:var(--o-well)"><span class="o-mut">{{ p.sourceDeleted ? 'Branch ' + p.source + ' deleted.' : 'Branch ' + p.source + ' can be deleted.' }}</span>@if (p.mergedSha) {<a class="o-btn o-r" [routerLink]="link('commit/' + p.mergedSha)">View {{ p.mergedSha.slice(0, 7) }}</a>}</div>
|
|
2572
|
+
}
|
|
2573
|
+
</div>
|
|
2574
|
+
|
|
2575
|
+
<div class="o-card">
|
|
2576
|
+
<label class="o-sr-only" for="pr-cmt">Add a comment</label>
|
|
2577
|
+
<textarea id="pr-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;height:auto;padding:10px 12px" placeholder="Add a comment. Markdown works." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="pull-comment"></textarea>
|
|
2578
|
+
<div class="o-cf"><span>Markdown</span><button type="button" class="o-btn o-sm o-dark o-r" (click)="addComment()" [disabled]="!draft().trim() || busy()" data-testid="pull-comment-add">Comment</button></div>
|
|
2579
|
+
</div>
|
|
2580
|
+
}
|
|
2581
|
+
@case ('commits') {
|
|
2582
|
+
<div class="o-card">
|
|
2583
|
+
@for (c of cmp()?.commits ?? []; track c.oid) {
|
|
2584
|
+
<div class="o-row"><span class="o-av" aria-hidden="true">{{ initials(c.author.name) }}</span><a class="o-ell" style="flex:1;color:var(--o-tx)" [routerLink]="link('commit/' + c.oid)">{{ c.subject }}</a><a class="o-mono o-sm o-faint" [routerLink]="link('commit/' + c.oid)">{{ c.short }}</a></div>
|
|
2585
|
+
} @empty {<div class="o-empty">{{ cmp() ? 'No commits.' : 'Loading…' }}</div>}
|
|
2586
|
+
</div>
|
|
2587
|
+
}
|
|
2588
|
+
@case ('files') {
|
|
2589
|
+
<div class="o-flex o-wrap"><span class="o-faint o-sm">{{ cmp()?.files?.length ?? 0 }} {{ cmp()?.files?.length === 1 ? 'file' : 'files' }} · click a line number to comment</span><span class="o-r o-faint o-sm">{{ openThreads() }} unresolved {{ openThreads() === 1 ? 'conversation' : 'conversations' }}</span></div>
|
|
2590
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2591
|
+
<o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" (comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" />
|
|
2592
|
+
} @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
|
|
2593
|
+
}
|
|
2594
|
+
@case ('checks') {
|
|
2595
|
+
<div class="o-card">
|
|
2596
|
+
@for (c of p.checks; track c.id) {
|
|
2597
|
+
<div class="o-row"><span [class]="dot(c.state)" aria-hidden="true"></span><span class="o-mono o-sm">{{ c.context }}</span><span class="o-faint">{{ c.description }}</span>@if (c.url) {<a class="o-r o-sm" [href]="c.url" target="_blank" rel="noopener">Details</a>}</div>
|
|
2598
|
+
} @empty {<div class="o-empty">No checks reported on {{ p.headSha.slice(0, 7) }}. Pipelines report here once Deploy is set up.</div>}
|
|
2599
|
+
</div>
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
</div>
|
|
2603
|
+
|
|
2604
|
+
<div class="o-col" style="gap:18px;font-size:13px">
|
|
2605
|
+
<div class="o-col">
|
|
2606
|
+
<span class="o-lbl">Reviewers</span>
|
|
2607
|
+
@for (r of reviewers(); track r.id) {<div class="o-flex"><span class="o-av" aria-hidden="true">{{ people().initials(r.id) }}</span><span>{{ people().name(r.id) }}</span><span class="o-r o-sm" [style.color]="r.colour">{{ r.label }}</span></div>}
|
|
2608
|
+
@if (!reviewers().length) {<span class="o-faint o-sm">No reviewers yet.</span>}
|
|
2609
|
+
<span class="o-faint o-xs">CODEOWNERS requests owners of changed paths at GA.</span>
|
|
2610
|
+
</div>
|
|
2611
|
+
<div class="o-col"><span class="o-lbl">Labels</span><div class="o-flex o-wrap">@for (l of p.labels; track l) {<span class="o-chip o-out">{{ l }}</span>}@if (!p.labels.length) {<span class="o-faint o-sm">None</span>}</div></div>
|
|
2612
|
+
<div class="o-col">
|
|
2613
|
+
<span class="o-lbl">Linked issues</span>
|
|
2614
|
+
@for (k of p.keys; track k) {<a class="o-card o-cb o-click o-col" style="gap:4px;padding:10px 12px;color:var(--o-tx)" [routerLink]="host.link('issues/' + k)"><span class="o-mono o-xs o-mut">{{ k }}</span></a>}
|
|
2615
|
+
@if (!p.keys.length) {<span class="o-faint o-sm">Name an issue key in the title or description to link it.</span>}
|
|
2616
|
+
</div>
|
|
2617
|
+
<div class="o-col"><span class="o-lbl">Deployments</span><span class="o-faint o-sm">Releases that contain this pull request appear here after merge.</span></div>
|
|
2618
|
+
@if (canProtect()) {<div class="o-col"><span class="o-lbl">Branch protection · {{ p.target }}</span><a class="o-sm" [routerLink]="link('settings')" [queryParams]="{ section: 'protect' }">Edit rules</a></div>}
|
|
2619
|
+
</div>
|
|
2620
|
+
</div>
|
|
2621
|
+
} @else if (!error()) {
|
|
2622
|
+
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
2623
|
+
}
|
|
2624
|
+
</div>
|
|
2625
|
+
`, styles: [".badge{padding:3px 10px;border-radius:12px;font-size:12px;font-weight:500;color:var(--o-acc-fg)}.anchor{position:relative;align-self:center}.merge{display:flex;align-items:center;position:relative}.merge .go{height:32px;padding:0 14px;border-radius:7px 0 0 7px;border:none;font-size:13px;font-weight:500;color:var(--o-acc-fg);background:var(--o-ok);cursor:pointer}.merge .mn{height:32px;padding:0 10px;border:none;border-radius:0 7px 7px 0;font-size:12px;color:var(--o-acc-fg);background:var(--o-ok);border-left:1px solid rgba(255,255,255,.25);cursor:pointer}.merge button:disabled{background:var(--o-line-mid);cursor:not-allowed}.tl{display:flex;flex-direction:column;gap:10px;padding-left:14px;border-left:2px solid var(--o-line);margin-left:14px}\n"] }]
|
|
2626
|
+
}], ctorParameters: () => [], propDecorators: { outside: [{
|
|
2627
|
+
type: HostListener,
|
|
2628
|
+
args: ['document:click', ['$event']]
|
|
2629
|
+
}] } });
|
|
2630
|
+
|
|
2631
|
+
const BLANK = { id: '', pattern: '', blockDirectPush: true, blockForcePush: true, blockDeletion: true, requiredApprovals: 1, dismissStaleApprovals: true, requireUpToDate: false, requiredChecks: [] };
|
|
2632
|
+
// /{org}/{project}/code/{repo}/settings?section= (prototype "code_settings"): general settings and
|
|
2633
|
+
// merge methods, branch protection rules, and the danger zone (archive, delete).
|
|
2634
|
+
class RepoSettingsPage extends RepoPage {
|
|
2635
|
+
constructor() {
|
|
2636
|
+
super();
|
|
2637
|
+
this.router = inject(Router);
|
|
2638
|
+
this.section = signal('general', ...(ngDevMode ? [{ debugName: "section" }] : /* istanbul ignore next */ []));
|
|
2639
|
+
this.branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
|
|
2640
|
+
this.rules = signal([], ...(ngDevMode ? [{ debugName: "rules" }] : /* istanbul ignore next */ []));
|
|
2641
|
+
this.form = signal(null, ...(ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []));
|
|
2642
|
+
this.check = signal('', ...(ngDevMode ? [{ debugName: "check" }] : /* istanbul ignore next */ []));
|
|
2643
|
+
this.confirm = signal('', ...(ngDevMode ? [{ debugName: "confirm" }] : /* istanbul ignore next */ []));
|
|
2644
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
2645
|
+
this.g = { name: '', description: '', visibility: 'private', defaultBranch: '', mergeMethods: [], deleteBranchOnMerge: false };
|
|
2646
|
+
this.methods = [{ id: 'merge', label: 'Merge commit' }, { id: 'squash', label: 'Squash' }, { id: 'rebase', label: 'Rebase' }];
|
|
2647
|
+
this.toggles = [
|
|
2648
|
+
{ key: 'blockDirectPush', label: 'Require a pull request before merging', hint: 'Direct pushes are blocked; changes arrive through pull requests.' },
|
|
2649
|
+
{ key: 'blockForcePush', label: 'Block force pushes', hint: 'History on matching branches cannot be rewritten.' },
|
|
2650
|
+
{ key: 'blockDeletion', label: 'Block deletion', hint: 'Matching branches cannot be deleted.' },
|
|
2651
|
+
{ key: 'dismissStaleApprovals', label: 'Dismiss stale approvals', hint: 'A new commit dismisses approvals given on an older one.' },
|
|
2652
|
+
{ key: 'requireUpToDate', label: 'Require branches to be up to date', hint: 'The branch must contain the target’s latest commit before it merges.' },
|
|
2653
|
+
];
|
|
2654
|
+
this.route.queryParamMap.subscribe((q) => this.section.set(q.get('section') || 'general'));
|
|
2655
|
+
}
|
|
2656
|
+
onRepo(r) {
|
|
2657
|
+
this.g = { name: r.name, description: r.description, visibility: r.visibility, defaultBranch: r.defaultBranch, mergeMethods: [...(r.mergeMethods ?? [])], deleteBranchOnMerge: r.deleteBranchOnMerge };
|
|
2658
|
+
if (!r.empty)
|
|
2659
|
+
this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.branches.set(b), error: () => undefined });
|
|
2660
|
+
this.api.protections(this.projectId(), r.name).subscribe({ next: (p) => this.rules.set(p), error: () => undefined });
|
|
2661
|
+
}
|
|
2662
|
+
go(s) {
|
|
2663
|
+
void this.router.navigate([], { relativeTo: this.route, queryParams: { section: s === 'general' ? null : s }, replaceUrl: true });
|
|
2664
|
+
}
|
|
2665
|
+
max0(n) {
|
|
2666
|
+
return Math.max(0, n);
|
|
2667
|
+
}
|
|
2668
|
+
flipMethod(id) {
|
|
2669
|
+
this.g.mergeMethods = this.g.mergeMethods.includes(id) ? this.g.mergeMethods.filter((m) => m !== id) : [...this.g.mergeMethods, id];
|
|
2670
|
+
}
|
|
2671
|
+
summary(p) {
|
|
2672
|
+
const parts = [];
|
|
2673
|
+
if (p.requiredApprovals)
|
|
2674
|
+
parts.push(`${p.requiredApprovals} ${p.requiredApprovals === 1 ? 'approval' : 'approvals'}`);
|
|
2675
|
+
if (p.requiredChecks?.length)
|
|
2676
|
+
parts.push(`checks ${p.requiredChecks.join(', ')}`);
|
|
2677
|
+
if (p.blockDirectPush)
|
|
2678
|
+
parts.push('no direct push');
|
|
2679
|
+
if (p.requireUpToDate)
|
|
2680
|
+
parts.push('up to date');
|
|
2681
|
+
if (p.blockDeletion)
|
|
2682
|
+
parts.push('no deletion');
|
|
2683
|
+
return parts.join(' · ') || 'no restrictions';
|
|
2684
|
+
}
|
|
2685
|
+
edit(p) {
|
|
2686
|
+
this.form.set(p ? { ...BLANK, ...p, requiredChecks: [...(p.requiredChecks ?? [])] } : { ...BLANK, pattern: this.rules().length ? '' : this.repo()?.defaultBranch ?? 'main', requiredChecks: [] });
|
|
2687
|
+
}
|
|
2688
|
+
flip(f, k) {
|
|
2689
|
+
f[k] = !f[k];
|
|
2690
|
+
this.form.set({ ...f });
|
|
2691
|
+
}
|
|
2692
|
+
addCheck(f) {
|
|
2693
|
+
const c = this.check().trim();
|
|
2694
|
+
if (c && !f.requiredChecks.includes(c))
|
|
2695
|
+
f.requiredChecks = [...f.requiredChecks, c];
|
|
2696
|
+
this.check.set('');
|
|
2697
|
+
this.form.set({ ...f });
|
|
2698
|
+
}
|
|
2699
|
+
removeCheck(f, c) {
|
|
2700
|
+
f.requiredChecks = f.requiredChecks.filter((x) => x !== c);
|
|
2701
|
+
this.form.set({ ...f });
|
|
2702
|
+
}
|
|
2703
|
+
saveGeneral() {
|
|
2704
|
+
const r = this.repo();
|
|
2705
|
+
if (!r)
|
|
2706
|
+
return;
|
|
2707
|
+
this.busy.set(true);
|
|
2708
|
+
const renamed = this.g.name.trim() !== r.name;
|
|
2709
|
+
this.api.update(this.projectId(), r.name, { name: this.g.name.trim(), description: this.g.description, visibility: this.g.visibility, defaultBranch: this.g.defaultBranch, mergeMethods: this.g.mergeMethods, deleteBranchOnMerge: this.g.deleteBranchOnMerge }).subscribe({
|
|
2710
|
+
next: (n) => {
|
|
2711
|
+
this.busy.set(false);
|
|
2712
|
+
this.note.set('Saved.');
|
|
2713
|
+
if (renamed)
|
|
2714
|
+
void this.router.navigate(this.host.link(`code/${n.name}/settings`));
|
|
2715
|
+
else {
|
|
2716
|
+
this.repo.set(n);
|
|
2717
|
+
this.onRepo(n);
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not save.')); },
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
saveRule() {
|
|
2724
|
+
const r = this.repo();
|
|
2725
|
+
const f = this.form();
|
|
2726
|
+
if (!r || !f)
|
|
2727
|
+
return;
|
|
2728
|
+
const { id, ...body } = f;
|
|
2729
|
+
this.busy.set(true);
|
|
2730
|
+
const req = id ? this.api.updateProtection(this.projectId(), r.name, id, body) : this.api.addProtection(this.projectId(), r.name, body);
|
|
2731
|
+
req.subscribe({
|
|
2732
|
+
next: () => { this.busy.set(false); this.form.set(null); this.note.set(`Rule “${f.pattern}” saved.`); this.onRepo(r); },
|
|
2733
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not save the rule.')); },
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
removeRule(f) {
|
|
2737
|
+
const r = this.repo();
|
|
2738
|
+
if (!r || !f.id)
|
|
2739
|
+
return;
|
|
2740
|
+
this.api.removeProtection(this.projectId(), r.name, f.id).subscribe({
|
|
2741
|
+
next: () => { this.form.set(null); this.note.set(`Rule “${f.pattern}” deleted.`); this.onRepo(r); },
|
|
2742
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not delete the rule.')),
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
archive(on) {
|
|
2746
|
+
const r = this.repo();
|
|
2747
|
+
if (!r)
|
|
2748
|
+
return;
|
|
2749
|
+
this.busy.set(true);
|
|
2750
|
+
this.api.setArchived(this.projectId(), r.name, on).subscribe({
|
|
2751
|
+
next: (n) => { this.busy.set(false); this.repo.set(n); this.note.set(on ? 'Archived: the repository is read-only.' : 'Unarchived.'); },
|
|
2752
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not change the archive state.')); },
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
remove() {
|
|
2756
|
+
const r = this.repo();
|
|
2757
|
+
if (!r)
|
|
2758
|
+
return;
|
|
2759
|
+
this.busy.set(true);
|
|
2760
|
+
this.api.remove(this.projectId(), r.name).subscribe({
|
|
2761
|
+
next: () => { this.busy.set(false); void this.router.navigate(this.host.link('code')); },
|
|
2762
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not delete the repository.')); },
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoSettingsPage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2766
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: RepoSettingsPage, isStandalone: true, selector: "o-code-settings", usesInheritance: true, ngImport: i0, template: `
|
|
2767
|
+
<div class="o-page">
|
|
2768
|
+
@if (repo(); as r) {
|
|
2769
|
+
<div class="o-split o-lft">
|
|
2770
|
+
<nav class="o-setnav" aria-label="Repository settings">
|
|
2771
|
+
<span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
|
|
2772
|
+
<button type="button" [class.o-on]="section() === 'general'" (click)="go('general')">General</button>
|
|
2773
|
+
<button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
|
|
2774
|
+
<button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators<b class="o-ph-tag o-ga o-r">GA</b></button>
|
|
2775
|
+
<button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
|
|
2776
|
+
</nav>
|
|
2777
|
+
<div class="o-col" style="gap:16px;min-width:0">
|
|
2778
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2779
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
2780
|
+
@switch (section()) {
|
|
2781
|
+
@case ('general') {
|
|
2782
|
+
<h1 class="o-t">General</h1>
|
|
2783
|
+
<form class="o-card o-cb o-form" (ngSubmit)="saveGeneral()">
|
|
2784
|
+
<div class="o-g2">
|
|
2785
|
+
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!canAdmin()" /><span class="o-hint">Renaming keeps the old clone URL working as a redirect.</span></div>
|
|
2786
|
+
<div class="o-fld"><label for="rs-def">Default branch</label><select id="rs-def" class="o-in" name="def" [(ngModel)]="g.defaultBranch" [disabled]="!canAdmin()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}@if (!branches().length) {<option [value]="r.defaultBranch">{{ r.defaultBranch }}</option>}</select></div>
|
|
2787
|
+
</div>
|
|
2788
|
+
<div class="o-fld"><label for="rs-desc">Description</label><input id="rs-desc" class="o-in" name="desc" [(ngModel)]="g.description" [disabled]="!canAdmin()" /></div>
|
|
2789
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
2790
|
+
<div class="o-g2">
|
|
2791
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!canAdmin()"><span class="o-rad" [class.o-on]="g.visibility === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
2792
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!canAdmin()"><span class="o-rad" [class.o-on]="g.visibility === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
2793
|
+
</div>
|
|
2794
|
+
</div>
|
|
2795
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed merge methods</span>
|
|
2796
|
+
@for (m of methods; track m.id) {<label class="o-flex"><input type="checkbox" [checked]="g.mergeMethods.includes(m.id)" (change)="flipMethod(m.id)" [disabled]="!canAdmin()" />{{ m.label }}</label>}
|
|
2797
|
+
</div>
|
|
2798
|
+
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-del" [class.o-on]="g.deleteBranchOnMerge" [attr.aria-checked]="g.deleteBranchOnMerge" (click)="g.deleteBranchOnMerge = !g.deleteBranchOnMerge" [disabled]="!canAdmin()"></button><span id="rs-del">Delete head branches after merge</span></div>
|
|
2799
|
+
<div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Allow auto-merge"></button>Allow auto-merge when all requirements pass<b class="o-ph-tag o-ga">GA</b></div>
|
|
2800
|
+
@if (canAdmin()) {<div class="o-flex"><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !g.mergeMethods.length">Save</button></div>}
|
|
2801
|
+
</form>
|
|
2802
|
+
}
|
|
2803
|
+
@case ('protect') {
|
|
2804
|
+
<div class="o-flex"><h1 class="o-t">Branch protection</h1>@if (canProtect()) {<button type="button" class="o-btn o-sm o-r" (click)="edit(null)" data-testid="add-rule">+ Add rule</button>}</div>
|
|
2805
|
+
<div class="o-card">
|
|
2806
|
+
@for (p of rules(); track p.id) {
|
|
2807
|
+
<button type="button" class="o-row o-click" style="width:100%;border:none;background:none;text-align:left" [class.o-sel]="form()?.id === p.id" (click)="edit(p)"><b class="o-mono">{{ p.pattern }}</b><span class="o-faint o-sm">{{ summary(p) }}</span>@if (form()?.id === p.id) {<span class="o-r o-chip">Editing</span>}</button>
|
|
2808
|
+
} @empty {<div class="o-empty">No rules. Without one, anyone who can push may push to any branch.</div>}
|
|
2809
|
+
</div>
|
|
2810
|
+
@if (form(); as f) {
|
|
2811
|
+
<form class="o-card" (ngSubmit)="saveRule()" aria-label="Protection rule">
|
|
2812
|
+
<div class="o-ch"><b>Rule for</b><input class="o-in o-mono o-sm" style="width:200px" name="pattern" aria-label="Branch name or pattern" [(ngModel)]="f.pattern" placeholder="main or release/*" data-testid="rule-pattern" /></div>
|
|
2813
|
+
<div class="o-cb o-form">
|
|
2814
|
+
@for (t of toggles; track t.key) {
|
|
2815
|
+
<div class="o-flex" style="align-items:flex-start"><button type="button" class="o-tog" role="switch" [attr.aria-label]="t.label" [class.o-on]="f[t.key]" [attr.aria-checked]="f[t.key]" (click)="flip(f, t.key)" style="margin-top:2px"></button><div class="o-col" style="gap:2px;flex:1"><span class="o-b5">{{ t.label }}</span><span class="o-hint">{{ t.hint }}</span></div></div>
|
|
2816
|
+
}
|
|
2817
|
+
<div class="o-flex"><span class="o-b5" style="flex:1">Required approvals</span><button type="button" class="o-btn o-sm" (click)="f.requiredApprovals = max0(f.requiredApprovals - 1)" aria-label="One fewer">−</button><b class="o-mono" style="width:24px;text-align:center" aria-live="polite">{{ f.requiredApprovals }}</b><button type="button" class="o-btn o-sm" (click)="f.requiredApprovals = f.requiredApprovals + 1" aria-label="One more">+</button></div>
|
|
2818
|
+
<div class="o-fld"><label for="rule-check">Required status checks</label>
|
|
2819
|
+
<div class="o-flex o-wrap">
|
|
2820
|
+
@for (c of f.requiredChecks; track c) {<span class="o-chip o-mono">{{ c }} <button type="button" class="o-lnk" style="font-size:11px" (click)="removeCheck(f, c)" [attr.aria-label]="'Remove ' + c">✕</button></span>}
|
|
2821
|
+
<input id="rule-check" class="o-in o-sm o-mono" style="width:160px" placeholder="+ check name, Enter" [ngModel]="check()" (ngModelChange)="check.set($event)" name="check" (keydown.enter)="$event.preventDefault(); addCheck(f)" />
|
|
2822
|
+
</div>
|
|
2823
|
+
</div>
|
|
2824
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass <b class="o-ph-tag o-ga">GA</b></span><div class="o-flex"><span class="o-chip">Owners</span><span class="o-hint">Every bypass is written to the audit log.</span></div></div>
|
|
2825
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">What a blocked push sees</span><div class="o-term" style="font-size:11.5px"><div>$ git push origin {{ f.pattern || 'main' }}</div><div>remote: ✕ {{ f.pattern || 'main' }} is protected by rule “{{ f.pattern || 'main' }}”: direct pushes are blocked.</div><div>remote: Open a pull request instead.</div></div></div>
|
|
2826
|
+
<div class="o-flex">
|
|
2827
|
+
@if (f.id) {<button type="button" class="o-btn o-sm o-ghost" style="color:var(--o-bad-tx)" (click)="removeRule(f)">Delete rule</button>}
|
|
2828
|
+
<button type="button" class="o-btn o-sm o-r" (click)="form.set(null)">Cancel</button>
|
|
2829
|
+
<button type="submit" class="o-btn o-dark" [disabled]="busy() || !f.pattern.trim()" data-testid="rule-save">Save rule</button>
|
|
2830
|
+
</div>
|
|
2831
|
+
</div>
|
|
2832
|
+
</form>
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
@case ('collab') {
|
|
2836
|
+
<h1 class="o-t">Collaborators</h1>
|
|
2837
|
+
<div class="o-card"><div class="o-empty">Access beyond the project role, for one repository, arrives at general availability. Until then, project roles decide who reads and pushes.</div></div>
|
|
2838
|
+
}
|
|
2839
|
+
@case ('danger') {
|
|
2840
|
+
<h1 class="o-t">Danger zone</h1>
|
|
2841
|
+
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
2842
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!canAdmin() || busy()">{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</button></div>
|
|
2843
|
+
<div class="o-row" style="padding:14px;flex-wrap:wrap"><div class="o-col" style="gap:2px;flex:1;min-width:240px"><b>Delete this repository</b><span class="o-hint">Recoverable for 7 days, then gone for good. Type <b class="o-mono">{{ r.name }}</b> to confirm.</span></div>
|
|
2844
|
+
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!canAdmin()" />
|
|
2845
|
+
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!canAdmin() || confirm() !== r.name || busy()">Delete</button>
|
|
2846
|
+
</div>
|
|
2847
|
+
</div>
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
</div>
|
|
2851
|
+
</div>
|
|
2852
|
+
} @else if (error()) {
|
|
2853
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
2854
|
+
}
|
|
2855
|
+
</div>
|
|
2856
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2857
|
+
}
|
|
2858
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: RepoSettingsPage, decorators: [{
|
|
2859
|
+
type: Component,
|
|
2860
|
+
args: [{
|
|
2861
|
+
selector: 'o-code-settings',
|
|
2862
|
+
imports: [FormsModule],
|
|
2863
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2864
|
+
template: `
|
|
2865
|
+
<div class="o-page">
|
|
2866
|
+
@if (repo(); as r) {
|
|
2867
|
+
<div class="o-split o-lft">
|
|
2868
|
+
<nav class="o-setnav" aria-label="Repository settings">
|
|
2869
|
+
<span class="o-lbl" style="padding-top:0">{{ r.name }}</span>
|
|
2870
|
+
<button type="button" [class.o-on]="section() === 'general'" (click)="go('general')">General</button>
|
|
2871
|
+
<button type="button" [class.o-on]="section() === 'protect'" (click)="go('protect')">Branch protection</button>
|
|
2872
|
+
<button type="button" [class.o-on]="section() === 'collab'" (click)="go('collab')">Collaborators<b class="o-ph-tag o-ga o-r">GA</b></button>
|
|
2873
|
+
<button type="button" [class.o-on]="section() === 'danger'" (click)="go('danger')">Danger zone</button>
|
|
2874
|
+
</nav>
|
|
2875
|
+
<div class="o-col" style="gap:16px;min-width:0">
|
|
2876
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
2877
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
2878
|
+
@switch (section()) {
|
|
2879
|
+
@case ('general') {
|
|
2880
|
+
<h1 class="o-t">General</h1>
|
|
2881
|
+
<form class="o-card o-cb o-form" (ngSubmit)="saveGeneral()">
|
|
2882
|
+
<div class="o-g2">
|
|
2883
|
+
<div class="o-fld"><label for="rs-name">Repository name</label><input id="rs-name" class="o-in o-mono" name="name" [(ngModel)]="g.name" [disabled]="!canAdmin()" /><span class="o-hint">Renaming keeps the old clone URL working as a redirect.</span></div>
|
|
2884
|
+
<div class="o-fld"><label for="rs-def">Default branch</label><select id="rs-def" class="o-in" name="def" [(ngModel)]="g.defaultBranch" [disabled]="!canAdmin()">@for (b of branches(); track b.name) {<option [value]="b.name">{{ b.name }}</option>}@if (!branches().length) {<option [value]="r.defaultBranch">{{ r.defaultBranch }}</option>}</select></div>
|
|
2885
|
+
</div>
|
|
2886
|
+
<div class="o-fld"><label for="rs-desc">Description</label><input id="rs-desc" class="o-in" name="desc" [(ngModel)]="g.description" [disabled]="!canAdmin()" /></div>
|
|
2887
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Visibility</span>
|
|
2888
|
+
<div class="o-g2">
|
|
2889
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'private'" (click)="g.visibility = 'private'" [disabled]="!canAdmin()"><span class="o-rad" [class.o-on]="g.visibility === 'private'"></span><span class="o-col" style="gap:2px"><b>Private</b><span class="o-faint o-sm">Project members only</span></span></button>
|
|
2890
|
+
<button type="button" class="o-opt" [class.o-on]="g.visibility === 'internal'" (click)="g.visibility = 'internal'" [disabled]="!canAdmin()"><span class="o-rad" [class.o-on]="g.visibility === 'internal'"></span><span class="o-col" style="gap:2px"><b>Org-internal</b><span class="o-faint o-sm">Everyone in the organization can read</span></span></button>
|
|
2891
|
+
</div>
|
|
2892
|
+
</div>
|
|
2893
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed merge methods</span>
|
|
2894
|
+
@for (m of methods; track m.id) {<label class="o-flex"><input type="checkbox" [checked]="g.mergeMethods.includes(m.id)" (change)="flipMethod(m.id)" [disabled]="!canAdmin()" />{{ m.label }}</label>}
|
|
2895
|
+
</div>
|
|
2896
|
+
<div class="o-flex"><button type="button" class="o-tog" role="switch" aria-labelledby="rs-del" [class.o-on]="g.deleteBranchOnMerge" [attr.aria-checked]="g.deleteBranchOnMerge" (click)="g.deleteBranchOnMerge = !g.deleteBranchOnMerge" [disabled]="!canAdmin()"></button><span id="rs-del">Delete head branches after merge</span></div>
|
|
2897
|
+
<div class="o-flex"><button type="button" class="o-tog o-dis" disabled aria-label="Allow auto-merge"></button>Allow auto-merge when all requirements pass<b class="o-ph-tag o-ga">GA</b></div>
|
|
2898
|
+
@if (canAdmin()) {<div class="o-flex"><button type="submit" class="o-btn o-dark o-r" [disabled]="busy() || !g.mergeMethods.length">Save</button></div>}
|
|
2899
|
+
</form>
|
|
2900
|
+
}
|
|
2901
|
+
@case ('protect') {
|
|
2902
|
+
<div class="o-flex"><h1 class="o-t">Branch protection</h1>@if (canProtect()) {<button type="button" class="o-btn o-sm o-r" (click)="edit(null)" data-testid="add-rule">+ Add rule</button>}</div>
|
|
2903
|
+
<div class="o-card">
|
|
2904
|
+
@for (p of rules(); track p.id) {
|
|
2905
|
+
<button type="button" class="o-row o-click" style="width:100%;border:none;background:none;text-align:left" [class.o-sel]="form()?.id === p.id" (click)="edit(p)"><b class="o-mono">{{ p.pattern }}</b><span class="o-faint o-sm">{{ summary(p) }}</span>@if (form()?.id === p.id) {<span class="o-r o-chip">Editing</span>}</button>
|
|
2906
|
+
} @empty {<div class="o-empty">No rules. Without one, anyone who can push may push to any branch.</div>}
|
|
2907
|
+
</div>
|
|
2908
|
+
@if (form(); as f) {
|
|
2909
|
+
<form class="o-card" (ngSubmit)="saveRule()" aria-label="Protection rule">
|
|
2910
|
+
<div class="o-ch"><b>Rule for</b><input class="o-in o-mono o-sm" style="width:200px" name="pattern" aria-label="Branch name or pattern" [(ngModel)]="f.pattern" placeholder="main or release/*" data-testid="rule-pattern" /></div>
|
|
2911
|
+
<div class="o-cb o-form">
|
|
2912
|
+
@for (t of toggles; track t.key) {
|
|
2913
|
+
<div class="o-flex" style="align-items:flex-start"><button type="button" class="o-tog" role="switch" [attr.aria-label]="t.label" [class.o-on]="f[t.key]" [attr.aria-checked]="f[t.key]" (click)="flip(f, t.key)" style="margin-top:2px"></button><div class="o-col" style="gap:2px;flex:1"><span class="o-b5">{{ t.label }}</span><span class="o-hint">{{ t.hint }}</span></div></div>
|
|
2914
|
+
}
|
|
2915
|
+
<div class="o-flex"><span class="o-b5" style="flex:1">Required approvals</span><button type="button" class="o-btn o-sm" (click)="f.requiredApprovals = max0(f.requiredApprovals - 1)" aria-label="One fewer">−</button><b class="o-mono" style="width:24px;text-align:center" aria-live="polite">{{ f.requiredApprovals }}</b><button type="button" class="o-btn o-sm" (click)="f.requiredApprovals = f.requiredApprovals + 1" aria-label="One more">+</button></div>
|
|
2916
|
+
<div class="o-fld"><label for="rule-check">Required status checks</label>
|
|
2917
|
+
<div class="o-flex o-wrap">
|
|
2918
|
+
@for (c of f.requiredChecks; track c) {<span class="o-chip o-mono">{{ c }} <button type="button" class="o-lnk" style="font-size:11px" (click)="removeCheck(f, c)" [attr.aria-label]="'Remove ' + c">✕</button></span>}
|
|
2919
|
+
<input id="rule-check" class="o-in o-sm o-mono" style="width:160px" placeholder="+ check name, Enter" [ngModel]="check()" (ngModelChange)="check.set($event)" name="check" (keydown.enter)="$event.preventDefault(); addCheck(f)" />
|
|
2920
|
+
</div>
|
|
2921
|
+
</div>
|
|
2922
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">Allowed to bypass <b class="o-ph-tag o-ga">GA</b></span><div class="o-flex"><span class="o-chip">Owners</span><span class="o-hint">Every bypass is written to the audit log.</span></div></div>
|
|
2923
|
+
<div class="o-fld"><span class="o-lbl" style="padding:0">What a blocked push sees</span><div class="o-term" style="font-size:11.5px"><div>$ git push origin {{ f.pattern || 'main' }}</div><div>remote: ✕ {{ f.pattern || 'main' }} is protected by rule “{{ f.pattern || 'main' }}”: direct pushes are blocked.</div><div>remote: Open a pull request instead.</div></div></div>
|
|
2924
|
+
<div class="o-flex">
|
|
2925
|
+
@if (f.id) {<button type="button" class="o-btn o-sm o-ghost" style="color:var(--o-bad-tx)" (click)="removeRule(f)">Delete rule</button>}
|
|
2926
|
+
<button type="button" class="o-btn o-sm o-r" (click)="form.set(null)">Cancel</button>
|
|
2927
|
+
<button type="submit" class="o-btn o-dark" [disabled]="busy() || !f.pattern.trim()" data-testid="rule-save">Save rule</button>
|
|
2928
|
+
</div>
|
|
2929
|
+
</div>
|
|
2930
|
+
</form>
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
@case ('collab') {
|
|
2934
|
+
<h1 class="o-t">Collaborators</h1>
|
|
2935
|
+
<div class="o-card"><div class="o-empty">Access beyond the project role, for one repository, arrives at general availability. Until then, project roles decide who reads and pushes.</div></div>
|
|
2936
|
+
}
|
|
2937
|
+
@case ('danger') {
|
|
2938
|
+
<h1 class="o-t">Danger zone</h1>
|
|
2939
|
+
<div class="o-card" style="border-color:var(--o-bad-line)">
|
|
2940
|
+
<div class="o-row" style="padding:14px"><div class="o-col" style="gap:2px;flex:1"><b>{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</b><span class="o-hint">{{ r.archivedAt ? 'Pushes are accepted again.' : 'Read-only. Pushes are rejected; pipelines stop triggering.' }}</span></div><button type="button" class="o-btn o-sm" (click)="archive(!r.archivedAt)" [disabled]="!canAdmin() || busy()">{{ r.archivedAt ? 'Unarchive' : 'Archive' }}</button></div>
|
|
2941
|
+
<div class="o-row" style="padding:14px;flex-wrap:wrap"><div class="o-col" style="gap:2px;flex:1;min-width:240px"><b>Delete this repository</b><span class="o-hint">Recoverable for 7 days, then gone for good. Type <b class="o-mono">{{ r.name }}</b> to confirm.</span></div>
|
|
2942
|
+
<input class="o-in o-sm o-mono" style="width:180px" aria-label="Type the repository name to confirm" [ngModel]="confirm()" (ngModelChange)="confirm.set($event)" [disabled]="!canAdmin()" />
|
|
2943
|
+
<button type="button" class="o-btn o-sm o-red" (click)="remove()" [disabled]="!canAdmin() || confirm() !== r.name || busy()">Delete</button>
|
|
2944
|
+
</div>
|
|
2945
|
+
</div>
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
</div>
|
|
2949
|
+
</div>
|
|
2950
|
+
} @else if (error()) {
|
|
2951
|
+
<div class="o-card o-empty"><p>{{ error() }}</p></div>
|
|
2952
|
+
}
|
|
2953
|
+
</div>
|
|
2954
|
+
`,
|
|
2955
|
+
}]
|
|
2956
|
+
}], ctorParameters: () => [] });
|
|
2957
|
+
|
|
2958
|
+
/*
|
|
2959
|
+
* @chisquare-tech/origine-ngx-apps-code: the Code module's screens (TRD §6.10). The host app provides
|
|
2960
|
+
* CodeHost and mounts the pages at /{org}/{project}/code, /code/new, /code/pulls and
|
|
2961
|
+
* /code/{repo}[/blob|/commits|/commit/:sha|/branches|/pulls|/pull/:number|/settings].
|
|
2962
|
+
*/
|
|
2963
|
+
|
|
2964
|
+
/**
|
|
2965
|
+
* Generated bundle index. Do not edit.
|
|
2966
|
+
*/
|
|
2967
|
+
|
|
2968
|
+
export { BranchesPage, CodeApi, CodeHost, CommitPage, CommitsPage, CurrentRepo, CurrentRepoPage, DiffView, FilePage, NewRepoPage, People, PullPage, PullsPage, RefPicker, RepoHomePage, RepoPage, RepoSettingsPage, ReposPage, TOKEN_CSS, ago, apiMessage, checkDot, copy, dayLabel, initials, langColour, shortDate, size };
|
|
2969
|
+
//# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map
|