@chisquare-tech/origine-ngx-apps-code 0.1.0 → 0.3.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.
|
@@ -1,10 +1,11 @@
|
|
|
1
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';
|
|
2
|
+
import { inject, Injectable, signal, computed, effect, untracked, Directive, ElementRef, input, output, HostListener, ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
|
|
3
3
|
import { ApiService } from '@thescaffold/ngx-core';
|
|
4
4
|
import { map, forkJoin, of } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/forms';
|
|
6
6
|
import { FormsModule } from '@angular/forms';
|
|
7
7
|
import { ActivatedRoute, RouterLink, Router } from '@angular/router';
|
|
8
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
8
9
|
import { map as map$1, catchError } from 'rxjs/operators';
|
|
9
10
|
import { marked } from 'marked';
|
|
10
11
|
|
|
@@ -95,10 +96,14 @@ class CodeApi {
|
|
|
95
96
|
return this.api.get(`${this.repoPath(project, name)}/commit/${enc(sha)}`).pipe(map((r) => data(r)));
|
|
96
97
|
}
|
|
97
98
|
/** One file's hunks from base to head ("" base: the head's first parent). */
|
|
98
|
-
diff(project, name, head, path, base = '') {
|
|
99
|
+
diff(project, name, head, path, base = '', opts = {}) {
|
|
99
100
|
const p = { head, path };
|
|
100
101
|
if (base)
|
|
101
102
|
p['base'] = base;
|
|
103
|
+
if (opts.context)
|
|
104
|
+
p['context'] = String(opts.context);
|
|
105
|
+
if (opts.ignoreSpace)
|
|
106
|
+
p['w'] = 'ignore';
|
|
102
107
|
return this.api.get(`${this.repoPath(project, name)}/diff`, p).pipe(map((r) => data(r)));
|
|
103
108
|
}
|
|
104
109
|
compare(project, name, base, head) {
|
|
@@ -110,6 +115,42 @@ class CodeApi {
|
|
|
110
115
|
p['q'] = q;
|
|
111
116
|
return this.api.get(`${this.repoPath(project, name)}/refs`, p).pipe(map((r) => data(r) ?? []));
|
|
112
117
|
}
|
|
118
|
+
createTag(project, name, tag, target) {
|
|
119
|
+
return this.api.post(`${this.repoPath(project, name)}/tags`, { tag, target }).pipe(map((r) => data(r)));
|
|
120
|
+
}
|
|
121
|
+
deleteTag(project, name, tag) {
|
|
122
|
+
return this.api.delete(`${this.repoPath(project, name)}/tags/${enc(tag)}`);
|
|
123
|
+
}
|
|
124
|
+
releases(project, name) {
|
|
125
|
+
return this.api.get(`${this.repoPath(project, name)}/releases`).pipe(map((r) => data(r) ?? []));
|
|
126
|
+
}
|
|
127
|
+
createRelease(project, name, r) {
|
|
128
|
+
return this.api.post(`${this.repoPath(project, name)}/releases`, r).pipe(map((x) => data(x)));
|
|
129
|
+
}
|
|
130
|
+
updateRelease(project, name, id, r) {
|
|
131
|
+
return this.api.patch(`${this.repoPath(project, name)}/releases/${enc(id)}`, r).pipe(map((x) => data(x)));
|
|
132
|
+
}
|
|
133
|
+
deleteRelease(project, name, id, deleteTag) {
|
|
134
|
+
return this.api.delete(`${this.repoPath(project, name)}/releases/${enc(id)}${deleteTag ? '?deleteTag=true' : ''}`);
|
|
135
|
+
}
|
|
136
|
+
/** Notes drafted from the done issues and merged pull requests since the previous release. */
|
|
137
|
+
releaseNotes(project, name, target, tag = '') {
|
|
138
|
+
return this.api.post(`${this.repoPath(project, name)}/releases/notes`, { target, tag: tag || undefined }).pipe(map((r) => data(r)?.notes ?? ''));
|
|
139
|
+
}
|
|
140
|
+
addAsset(project, name, id, file) {
|
|
141
|
+
return this.api
|
|
142
|
+
.post(`${this.repoPath(project, name)}/releases/${enc(id)}/assets?filename=${encodeURIComponent(file.name)}`, null, {
|
|
143
|
+
body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' },
|
|
144
|
+
})
|
|
145
|
+
.pipe(map((r) => data(r)));
|
|
146
|
+
}
|
|
147
|
+
/** A release file's bytes (the download needs the session, so no plain link works). */
|
|
148
|
+
assetBlob(project, name, id, asset) {
|
|
149
|
+
return this.api.get(`${this.repoPath(project, name)}/releases/${enc(id)}/assets/${enc(asset)}`, null, { responseType: 'blob' }).pipe(map((b) => b));
|
|
150
|
+
}
|
|
151
|
+
deleteAsset(project, name, id, asset) {
|
|
152
|
+
return this.api.delete(`${this.repoPath(project, name)}/releases/${enc(id)}/assets/${enc(asset)}`);
|
|
153
|
+
}
|
|
113
154
|
createBranch(project, name, branch, from) {
|
|
114
155
|
return this.api.post(`${this.repoPath(project, name)}/branches`, { name: branch, from });
|
|
115
156
|
}
|
|
@@ -153,6 +194,10 @@ class CodeApi {
|
|
|
153
194
|
comment(project, name, n, c) {
|
|
154
195
|
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/comments`, c).pipe(map((r) => data(r)));
|
|
155
196
|
}
|
|
197
|
+
/** Discards one of the caller's pending review comments. */
|
|
198
|
+
discardComment(project, name, n, id) {
|
|
199
|
+
return this.api.delete(`${this.repoPath(project, name)}/pulls/${n}/comments/${enc(id)}`);
|
|
200
|
+
}
|
|
156
201
|
review(project, name, n, state, body = '') {
|
|
157
202
|
return this.api.post(`${this.repoPath(project, name)}/pulls/${n}/reviews`, { state, body }).pipe(map((r) => data(r)));
|
|
158
203
|
}
|
|
@@ -414,22 +459,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
414
459
|
args: ['document:click', ['$event']]
|
|
415
460
|
}] } });
|
|
416
461
|
|
|
417
|
-
|
|
418
|
-
|
|
462
|
+
/** Past this many changed lines a file waits for "Load diff" (DEV-19 large diffs). */
|
|
463
|
+
const LARGE = 600;
|
|
464
|
+
/** Context steps for "Show more context". */
|
|
465
|
+
const CONTEXT = [3, 25, 100];
|
|
466
|
+
// One file of a diff (prototype prDiffs): its header with +/− and Viewed, the hunks highlighted,
|
|
467
|
+
// unified or split, and, on a pull request, the review threads on its lines and a comment form on
|
|
468
|
+
// any line number ("Add single comment" or part of a review). The diff loads when the file scrolls
|
|
469
|
+
// near the screen; a very large one waits to be asked for.
|
|
419
470
|
class DiffView {
|
|
420
471
|
constructor() {
|
|
421
472
|
this.api = inject(CodeApi);
|
|
422
473
|
this.host = inject(CodeHost);
|
|
474
|
+
this.el = inject((ElementRef));
|
|
475
|
+
this.destroyRef = inject(DestroyRef);
|
|
423
476
|
this.repo = input.required(...(ngDevMode ? [{ debugName: "repo" }] : /* istanbul ignore next */ []));
|
|
424
477
|
this.file = input.required(...(ngDevMode ? [{ debugName: "file" }] : /* istanbul ignore next */ []));
|
|
425
478
|
this.head = input.required(...(ngDevMode ? [{ debugName: "head" }] : /* istanbul ignore next */ []));
|
|
426
479
|
this.base = input('', ...(ngDevMode ? [{ debugName: "base" }] : /* istanbul ignore next */ []));
|
|
427
480
|
this.threads = input([], ...(ngDevMode ? [{ debugName: "threads" }] : /* istanbul ignore next */ []));
|
|
428
481
|
this.canComment = input(false, ...(ngDevMode ? [{ debugName: "canComment" }] : /* istanbul ignore next */ []));
|
|
482
|
+
this.mode = input('unified', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
483
|
+
this.ignoreSpace = input(false, ...(ngDevMode ? [{ debugName: "ignoreSpace" }] : /* istanbul ignore next */ []));
|
|
484
|
+
/** The file is marked viewed (pull requests); null hides the Viewed button. */
|
|
485
|
+
this.viewed = input(null, ...(ngDevMode ? [{ debugName: "viewed" }] : /* istanbul ignore next */ []));
|
|
486
|
+
/** The caller has a pending review on this pull request. */
|
|
487
|
+
this.reviewing = input(false, ...(ngDevMode ? [{ debugName: "reviewing" }] : /* istanbul ignore next */ []));
|
|
429
488
|
this.comment = output();
|
|
430
489
|
this.reply = output();
|
|
431
490
|
this.resolve = output();
|
|
491
|
+
this.discard = output();
|
|
492
|
+
this.viewedChange = output();
|
|
432
493
|
this.open = signal(true, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
494
|
+
this.visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
495
|
+
this.waiting = signal(false, ...(ngDevMode ? [{ debugName: "waiting" }] : /* istanbul ignore next */ []));
|
|
496
|
+
this.context = signal(CONTEXT[0], ...(ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []));
|
|
433
497
|
this.diff = signal(null, ...(ngDevMode ? [{ debugName: "diff" }] : /* istanbul ignore next */ []));
|
|
434
498
|
this.error = signal('', ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
435
499
|
this.drafting = signal('', ...(ngDevMode ? [{ debugName: "drafting" }] : /* istanbul ignore next */ []));
|
|
@@ -437,25 +501,82 @@ class DiffView {
|
|
|
437
501
|
this.replies = signal({}, ...(ngDevMode ? [{ debugName: "replies" }] : /* istanbul ignore next */ []));
|
|
438
502
|
this.people = computed(() => new People(this.host.people()), ...(ngDevMode ? [{ debugName: "people" }] : /* istanbul ignore next */ []));
|
|
439
503
|
this.threadsHere = computed(() => this.threads().filter((t) => t.path === this.file().path), ...(ngDevMode ? [{ debugName: "threadsHere" }] : /* istanbul ignore next */ []));
|
|
504
|
+
this.viewable = computed(() => this.viewed() !== null, ...(ngDevMode ? [{ debugName: "viewable" }] : /* istanbul ignore next */ []));
|
|
505
|
+
this.me = computed(() => this.host.meId(), ...(ngDevMode ? [{ debugName: "me" }] : /* istanbul ignore next */ []));
|
|
506
|
+
this.splitRows = computed(() => (this.diff()?.hunks ?? []).map((h) => pair(h.lines)), ...(ngDevMode ? [{ debugName: "splitRows" }] : /* istanbul ignore next */ []));
|
|
440
507
|
this.ago = (s) => ago(s);
|
|
508
|
+
this.asked = false;
|
|
509
|
+
effect(() => {
|
|
510
|
+
// viewed files fold away (and unfold when unmarked)
|
|
511
|
+
const v = this.viewed();
|
|
512
|
+
if (v !== null)
|
|
513
|
+
untracked(() => this.open.set(!v));
|
|
514
|
+
});
|
|
441
515
|
effect(() => {
|
|
442
|
-
|
|
443
|
-
|
|
516
|
+
// what the diff depends on; it loads once the file is near the screen and open
|
|
517
|
+
this.head();
|
|
518
|
+
this.base();
|
|
519
|
+
this.ignoreSpace();
|
|
520
|
+
this.context();
|
|
444
521
|
const f = this.file();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
522
|
+
if (!this.visible() || !this.open() || f.binary)
|
|
523
|
+
return;
|
|
524
|
+
untracked(() => {
|
|
525
|
+
if (!this.asked && f.additions + f.deletions > LARGE) {
|
|
526
|
+
this.waiting.set(true);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
this.load();
|
|
530
|
+
});
|
|
451
531
|
});
|
|
452
532
|
}
|
|
533
|
+
ngAfterViewInit() {
|
|
534
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
535
|
+
this.visible.set(true);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const io = new IntersectionObserver((entries) => {
|
|
539
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
540
|
+
this.visible.set(true);
|
|
541
|
+
io.disconnect();
|
|
542
|
+
}
|
|
543
|
+
}, { rootMargin: '600px 0px' });
|
|
544
|
+
io.observe(this.el.nativeElement);
|
|
545
|
+
this.destroyRef.onDestroy(() => io.disconnect());
|
|
546
|
+
}
|
|
547
|
+
load() {
|
|
548
|
+
const p = this.host.project();
|
|
549
|
+
const f = this.file();
|
|
550
|
+
if (!p || !this.head() || f.binary)
|
|
551
|
+
return;
|
|
552
|
+
this.asked = true;
|
|
553
|
+
this.waiting.set(false);
|
|
554
|
+
this.error.set('');
|
|
555
|
+
this.api.diff(p.id, this.repo(), this.head(), f.path, this.base(), { context: this.context(), ignoreSpace: this.ignoreSpace() }).subscribe({
|
|
556
|
+
next: (d) => this.diff.set(d),
|
|
557
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not load this diff.')),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
toggleOpen() {
|
|
561
|
+
this.open.set(!this.open());
|
|
562
|
+
}
|
|
563
|
+
moreContext() {
|
|
564
|
+
const next = CONTEXT.find((c) => c > this.context());
|
|
565
|
+
if (next)
|
|
566
|
+
this.context.set(next);
|
|
567
|
+
}
|
|
453
568
|
key(l) {
|
|
454
569
|
return `${l.kind === 'delete' ? 'old' : 'new'}:${this.lineOf(l)}`;
|
|
455
570
|
}
|
|
456
571
|
lineOf(l) {
|
|
457
572
|
return (l.kind === 'delete' ? l.old : l.new) ?? 0;
|
|
458
573
|
}
|
|
574
|
+
/** The lines of a split row that threads and drafts hang from (a context line once). */
|
|
575
|
+
rowLines(r) {
|
|
576
|
+
if (r.left && r.right && r.left === r.right)
|
|
577
|
+
return [r.left];
|
|
578
|
+
return [r.left, r.right].filter((l) => !!l);
|
|
579
|
+
}
|
|
459
580
|
threadsAt(l) {
|
|
460
581
|
const side = l.kind === 'delete' ? 'old' : 'new';
|
|
461
582
|
const n = this.lineOf(l);
|
|
@@ -467,11 +588,11 @@ class DiffView {
|
|
|
467
588
|
this.drafting.set(this.key(l));
|
|
468
589
|
this.draftBody.set('');
|
|
469
590
|
}
|
|
470
|
-
send(l) {
|
|
591
|
+
send(l, pending) {
|
|
471
592
|
const body = this.draftBody().trim();
|
|
472
593
|
if (!body)
|
|
473
594
|
return;
|
|
474
|
-
this.comment.emit({ path: this.file().path, side: l.kind === 'delete' ? 'old' : 'new', line: this.lineOf(l), body });
|
|
595
|
+
this.comment.emit({ path: this.file().path, side: l.kind === 'delete' ? 'old' : 'new', line: this.lineOf(l), body, pending });
|
|
475
596
|
this.drafting.set('');
|
|
476
597
|
this.draftBody.set('');
|
|
477
598
|
}
|
|
@@ -486,130 +607,254 @@ class DiffView {
|
|
|
486
607
|
this.setReply(id, '');
|
|
487
608
|
}
|
|
488
609
|
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">
|
|
610
|
+
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 }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, ignoreSpace: { classPropertyName: "ignoreSpace", publicName: "ignoreSpace", isSignal: true, isRequired: false, transformFunction: null }, viewed: { classPropertyName: "viewed", publicName: "viewed", isSignal: true, isRequired: false, transformFunction: null }, reviewing: { classPropertyName: "reviewing", publicName: "reviewing", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { comment: "comment", reply: "reply", resolve: "resolve", discard: "discard", viewedChange: "viewedChange" }, ngImport: i0, template: `
|
|
611
|
+
<div class="o-card" [attr.id]="'file-' + file().path" [attr.data-testid]="'diff-' + file().path">
|
|
491
612
|
<div class="o-ch" style="background:var(--o-well)">
|
|
492
|
-
<button type="button" class="o-btn o-ghost o-xs" (click)="
|
|
613
|
+
<button type="button" class="o-btn o-ghost o-xs" (click)="toggleOpen()" [attr.aria-expanded]="open()" [attr.aria-label]="(open() ? 'Collapse ' : 'Expand ') + file().path">{{ open() ? '▾' : '▸' }}</button>
|
|
493
614
|
<span class="o-mono o-sm o-ell">{{ file().oldPath && file().oldPath !== file().path ? file().oldPath + ' → ' : '' }}{{ file().path }}</span>
|
|
494
615
|
<span class="o-grn-t o-sm">+{{ file().additions }}</span><span class="o-red-t o-sm">−{{ file().deletions }}</span>
|
|
495
616
|
@if (threadsHere().length) {<span class="o-faint o-sm">💬 {{ threadsHere().length }}</span>}
|
|
496
|
-
<span class="o-r"
|
|
617
|
+
<span class="o-r o-flex" style="gap:6px">
|
|
618
|
+
@if (open() && diff() && context() < 100 && !file().binary) {<button type="button" class="o-btn o-xs" (click)="moreContext()" [attr.aria-label]="'Show more context in ' + file().path">More context</button>}
|
|
619
|
+
<ng-content />
|
|
620
|
+
@if (viewable()) {
|
|
621
|
+
<button type="button" class="o-btn o-xs viewed" role="checkbox" [attr.aria-checked]="viewed()" [attr.aria-label]="'Viewed ' + file().path" (click)="viewedChange.emit(!viewed())"><span class="o-cbx" [class.o-on]="viewed()" style="width:12px;height:12px"></span>Viewed</button>
|
|
622
|
+
}
|
|
623
|
+
</span>
|
|
497
624
|
</div>
|
|
498
625
|
@if (open()) {
|
|
499
626
|
@if (file().binary) {
|
|
500
627
|
<div class="o-empty o-sm">A binary file.</div>
|
|
628
|
+
} @else if (waiting()) {
|
|
629
|
+
<div class="o-empty o-sm o-col" style="gap:8px;align-items:center">
|
|
630
|
+
<span>{{ file().additions + file().deletions }} changed lines.</span>
|
|
631
|
+
<button type="button" class="o-btn o-sm" (click)="waiting.set(false); load()">Load diff</button>
|
|
632
|
+
</div>
|
|
501
633
|
} @else if (error()) {
|
|
502
634
|
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
503
635
|
} @else if (diff(); as d) {
|
|
504
636
|
@if (d.truncated) {<div class="o-note">This diff is too large to show in full.</div>}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
}
|
|
637
|
+
@if (mode() === 'split') {
|
|
638
|
+
<div class="o-code split" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path + ', side by side'">
|
|
639
|
+
@for (h of d.hunks; track $index; let hi = $index) {
|
|
640
|
+
<div class="hunk o-mono full" role="row">{{ h.header }}</div>
|
|
641
|
+
@for (r of splitRows()[hi]; track $index) {
|
|
642
|
+
@if (r.left; as l) {
|
|
643
|
+
<div class="o-ln side-l" role="row" [class.o-del]="l.kind === 'delete'" [attr.data-testid]="'split-old-' + l.old">
|
|
644
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.old }}</span>
|
|
645
|
+
<span class="o-sr-only">{{ l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
646
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
522
647
|
</div>
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
648
|
+
} @else {<div class="o-ln side-l empty-side" aria-hidden="true"><span class="o-n"></span><span class="o-t"></span></div>}
|
|
649
|
+
@if (r.right; as l) {
|
|
650
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [attr.data-testid]="'split-new-' + l.new">
|
|
651
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.new }}</span>
|
|
652
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : '' }}</span>
|
|
653
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
654
|
+
</div>
|
|
655
|
+
} @else {<div class="o-ln empty-side" aria-hidden="true"><span class="o-n"></span><span class="o-t"></span></div>}
|
|
656
|
+
@for (l of rowLines(r); track $index) {
|
|
657
|
+
@for (t of threadsAt(l); track t.id) {<div class="full"><ng-container *ngTemplateOutlet="threadTpl; context: { $implicit: t }" /></div>}
|
|
658
|
+
@if (drafting() === key(l)) {<div class="full"><ng-container *ngTemplateOutlet="draftTpl; context: { $implicit: l }" /></div>}
|
|
659
|
+
}
|
|
531
660
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
661
|
+
} @empty {<div class="o-empty o-sm full">{{ ignoreSpace() ? 'Only whitespace changed.' : 'No changes to show.' }}</div>}
|
|
662
|
+
</div>
|
|
663
|
+
} @else {
|
|
664
|
+
<div class="o-code" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path">
|
|
665
|
+
@for (h of d.hunks; track $index) {
|
|
666
|
+
<div class="hunk o-mono" role="row">{{ h.header }}</div>
|
|
667
|
+
@for (l of h.lines; track $index) {
|
|
668
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [class.o-del]="l.kind === 'delete'">
|
|
669
|
+
<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>
|
|
670
|
+
<span class="o-s" aria-hidden="true">{{ l.kind === 'add' ? '+' : l.kind === 'delete' ? '−' : ' ' }}</span>
|
|
671
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
672
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
673
|
+
</div>
|
|
674
|
+
@for (t of threadsAt(l); track t.id) {<ng-container *ngTemplateOutlet="threadTpl; context: { $implicit: t }" />}
|
|
675
|
+
@if (drafting() === key(l)) {<ng-container *ngTemplateOutlet="draftTpl; context: { $implicit: l }" />}
|
|
537
676
|
}
|
|
677
|
+
} @empty {
|
|
678
|
+
<div class="o-empty o-sm">{{ ignoreSpace() ? 'Only whitespace changed.' : 'No changes to show.' }}</div>
|
|
538
679
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
</div>
|
|
680
|
+
</div>
|
|
681
|
+
}
|
|
543
682
|
} @else {
|
|
544
683
|
<div class="o-empty o-sm">Loading…</div>
|
|
545
684
|
}
|
|
546
685
|
}
|
|
547
686
|
</div>
|
|
548
|
-
|
|
687
|
+
|
|
688
|
+
<ng-template #threadTpl let-t>
|
|
689
|
+
<div class="o-thread" [attr.data-testid]="'thread-' + t.id">
|
|
690
|
+
<div class="o-cb o-col" style="gap:6px;padding:10px 12px">
|
|
691
|
+
@for (c of t.comments; track c.id) {
|
|
692
|
+
<span class="o-sm"><b>{{ people().name(c.authorId) }}</b><span class="o-faint"> · {{ ago(c.createdAt) }}</span>
|
|
693
|
+
@if (c.pending) {<span class="o-chip o-amb" style="margin-left:6px">Pending</span>
|
|
694
|
+
@if (c.authorId === me()) {<button type="button" class="o-lnk" style="margin-left:6px;color:var(--o-mut)" (click)="discard.emit(c.id)" [attr.aria-label]="'Discard pending comment: ' + c.body">Discard</button>}
|
|
695
|
+
}
|
|
696
|
+
</span>
|
|
697
|
+
<span style="white-space:pre-wrap">{{ c.body }}</span>
|
|
698
|
+
}
|
|
699
|
+
</div>
|
|
700
|
+
@if (canComment()) {
|
|
701
|
+
<div class="o-cf">
|
|
702
|
+
<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)" />
|
|
703
|
+
<button type="button" class="o-btn o-sm" (click)="sendReply(t.id)">Reply</button>
|
|
704
|
+
<button type="button" class="o-btn o-sm" (click)="resolve.emit({ threadId: t.id, resolved: !t.resolved })">{{ t.resolved ? 'Unresolve' : 'Resolve' }}</button>
|
|
705
|
+
</div>
|
|
706
|
+
}
|
|
707
|
+
</div>
|
|
708
|
+
</ng-template>
|
|
709
|
+
|
|
710
|
+
<ng-template #draftTpl let-l>
|
|
711
|
+
<div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
|
|
712
|
+
<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>
|
|
713
|
+
<div class="o-flex o-wrap"><span class="o-faint o-xs">Markdown works.</span>
|
|
714
|
+
<button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button>
|
|
715
|
+
<button type="button" class="o-btn o-sm" [disabled]="!draftBody().trim()" (click)="send(l, false)" data-testid="line-comment-add">Add single comment</button>
|
|
716
|
+
<button type="button" class="o-btn o-sm o-pri" [disabled]="!draftBody().trim()" (click)="send(l, true)" data-testid="line-comment-review">{{ reviewing() ? 'Add review comment' : 'Start a review' }}</button>
|
|
717
|
+
</div>
|
|
718
|
+
</div></div>
|
|
719
|
+
</ng-template>
|
|
720
|
+
`, 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}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}\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: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
549
721
|
}
|
|
550
722
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DiffView, decorators: [{
|
|
551
723
|
type: Component,
|
|
552
|
-
args: [{ selector: 'o-diff', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
553
|
-
<div class="o-card" [attr.data-testid]="'diff-' + file().path">
|
|
724
|
+
args: [{ selector: 'o-diff', imports: [FormsModule, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
725
|
+
<div class="o-card" [attr.id]="'file-' + file().path" [attr.data-testid]="'diff-' + file().path">
|
|
554
726
|
<div class="o-ch" style="background:var(--o-well)">
|
|
555
|
-
<button type="button" class="o-btn o-ghost o-xs" (click)="
|
|
727
|
+
<button type="button" class="o-btn o-ghost o-xs" (click)="toggleOpen()" [attr.aria-expanded]="open()" [attr.aria-label]="(open() ? 'Collapse ' : 'Expand ') + file().path">{{ open() ? '▾' : '▸' }}</button>
|
|
556
728
|
<span class="o-mono o-sm o-ell">{{ file().oldPath && file().oldPath !== file().path ? file().oldPath + ' → ' : '' }}{{ file().path }}</span>
|
|
557
729
|
<span class="o-grn-t o-sm">+{{ file().additions }}</span><span class="o-red-t o-sm">−{{ file().deletions }}</span>
|
|
558
730
|
@if (threadsHere().length) {<span class="o-faint o-sm">💬 {{ threadsHere().length }}</span>}
|
|
559
|
-
<span class="o-r"
|
|
731
|
+
<span class="o-r o-flex" style="gap:6px">
|
|
732
|
+
@if (open() && diff() && context() < 100 && !file().binary) {<button type="button" class="o-btn o-xs" (click)="moreContext()" [attr.aria-label]="'Show more context in ' + file().path">More context</button>}
|
|
733
|
+
<ng-content />
|
|
734
|
+
@if (viewable()) {
|
|
735
|
+
<button type="button" class="o-btn o-xs viewed" role="checkbox" [attr.aria-checked]="viewed()" [attr.aria-label]="'Viewed ' + file().path" (click)="viewedChange.emit(!viewed())"><span class="o-cbx" [class.o-on]="viewed()" style="width:12px;height:12px"></span>Viewed</button>
|
|
736
|
+
}
|
|
737
|
+
</span>
|
|
560
738
|
</div>
|
|
561
739
|
@if (open()) {
|
|
562
740
|
@if (file().binary) {
|
|
563
741
|
<div class="o-empty o-sm">A binary file.</div>
|
|
742
|
+
} @else if (waiting()) {
|
|
743
|
+
<div class="o-empty o-sm o-col" style="gap:8px;align-items:center">
|
|
744
|
+
<span>{{ file().additions + file().deletions }} changed lines.</span>
|
|
745
|
+
<button type="button" class="o-btn o-sm" (click)="waiting.set(false); load()">Load diff</button>
|
|
746
|
+
</div>
|
|
564
747
|
} @else if (error()) {
|
|
565
748
|
<div class="o-note o-red" role="alert">{{ error() }}</div>
|
|
566
749
|
} @else if (diff(); as d) {
|
|
567
750
|
@if (d.truncated) {<div class="o-note">This diff is too large to show in full.</div>}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
}
|
|
751
|
+
@if (mode() === 'split') {
|
|
752
|
+
<div class="o-code split" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path + ', side by side'">
|
|
753
|
+
@for (h of d.hunks; track $index; let hi = $index) {
|
|
754
|
+
<div class="hunk o-mono full" role="row">{{ h.header }}</div>
|
|
755
|
+
@for (r of splitRows()[hi]; track $index) {
|
|
756
|
+
@if (r.left; as l) {
|
|
757
|
+
<div class="o-ln side-l" role="row" [class.o-del]="l.kind === 'delete'" [attr.data-testid]="'split-old-' + l.old">
|
|
758
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.old }}</span>
|
|
759
|
+
<span class="o-sr-only">{{ l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
760
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
585
761
|
</div>
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
762
|
+
} @else {<div class="o-ln side-l empty-side" aria-hidden="true"><span class="o-n"></span><span class="o-t"></span></div>}
|
|
763
|
+
@if (r.right; as l) {
|
|
764
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [attr.data-testid]="'split-new-' + l.new">
|
|
765
|
+
<span class="o-n" role="rowheader" [class.cmt]="canComment()" (click)="startComment(l)" [attr.title]="canComment() ? 'Comment on this line' : null">{{ l.new }}</span>
|
|
766
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : '' }}</span>
|
|
767
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
768
|
+
</div>
|
|
769
|
+
} @else {<div class="o-ln empty-side" aria-hidden="true"><span class="o-n"></span><span class="o-t"></span></div>}
|
|
770
|
+
@for (l of rowLines(r); track $index) {
|
|
771
|
+
@for (t of threadsAt(l); track t.id) {<div class="full"><ng-container *ngTemplateOutlet="threadTpl; context: { $implicit: t }" /></div>}
|
|
772
|
+
@if (drafting() === key(l)) {<div class="full"><ng-container *ngTemplateOutlet="draftTpl; context: { $implicit: l }" /></div>}
|
|
773
|
+
}
|
|
594
774
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
775
|
+
} @empty {<div class="o-empty o-sm full">{{ ignoreSpace() ? 'Only whitespace changed.' : 'No changes to show.' }}</div>}
|
|
776
|
+
</div>
|
|
777
|
+
} @else {
|
|
778
|
+
<div class="o-code" style="overflow-x:auto" role="table" [attr.aria-label]="'Changes to ' + file().path">
|
|
779
|
+
@for (h of d.hunks; track $index) {
|
|
780
|
+
<div class="hunk o-mono" role="row">{{ h.header }}</div>
|
|
781
|
+
@for (l of h.lines; track $index) {
|
|
782
|
+
<div class="o-ln" role="row" [class.o-add]="l.kind === 'add'" [class.o-del]="l.kind === 'delete'">
|
|
783
|
+
<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>
|
|
784
|
+
<span class="o-s" aria-hidden="true">{{ l.kind === 'add' ? '+' : l.kind === 'delete' ? '−' : ' ' }}</span>
|
|
785
|
+
<span class="o-sr-only">{{ l.kind === 'add' ? 'added' : l.kind === 'delete' ? 'removed' : '' }}</span>
|
|
786
|
+
<span class="o-t" role="cell" [innerHTML]="l.html || l.text"></span>
|
|
787
|
+
</div>
|
|
788
|
+
@for (t of threadsAt(l); track t.id) {<ng-container *ngTemplateOutlet="threadTpl; context: { $implicit: t }" />}
|
|
789
|
+
@if (drafting() === key(l)) {<ng-container *ngTemplateOutlet="draftTpl; context: { $implicit: l }" />}
|
|
600
790
|
}
|
|
791
|
+
} @empty {
|
|
792
|
+
<div class="o-empty o-sm">{{ ignoreSpace() ? 'Only whitespace changed.' : 'No changes to show.' }}</div>
|
|
601
793
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
</div>
|
|
794
|
+
</div>
|
|
795
|
+
}
|
|
606
796
|
} @else {
|
|
607
797
|
<div class="o-empty o-sm">Loading…</div>
|
|
608
798
|
}
|
|
609
799
|
}
|
|
610
800
|
</div>
|
|
611
|
-
|
|
612
|
-
|
|
801
|
+
|
|
802
|
+
<ng-template #threadTpl let-t>
|
|
803
|
+
<div class="o-thread" [attr.data-testid]="'thread-' + t.id">
|
|
804
|
+
<div class="o-cb o-col" style="gap:6px;padding:10px 12px">
|
|
805
|
+
@for (c of t.comments; track c.id) {
|
|
806
|
+
<span class="o-sm"><b>{{ people().name(c.authorId) }}</b><span class="o-faint"> · {{ ago(c.createdAt) }}</span>
|
|
807
|
+
@if (c.pending) {<span class="o-chip o-amb" style="margin-left:6px">Pending</span>
|
|
808
|
+
@if (c.authorId === me()) {<button type="button" class="o-lnk" style="margin-left:6px;color:var(--o-mut)" (click)="discard.emit(c.id)" [attr.aria-label]="'Discard pending comment: ' + c.body">Discard</button>}
|
|
809
|
+
}
|
|
810
|
+
</span>
|
|
811
|
+
<span style="white-space:pre-wrap">{{ c.body }}</span>
|
|
812
|
+
}
|
|
813
|
+
</div>
|
|
814
|
+
@if (canComment()) {
|
|
815
|
+
<div class="o-cf">
|
|
816
|
+
<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)" />
|
|
817
|
+
<button type="button" class="o-btn o-sm" (click)="sendReply(t.id)">Reply</button>
|
|
818
|
+
<button type="button" class="o-btn o-sm" (click)="resolve.emit({ threadId: t.id, resolved: !t.resolved })">{{ t.resolved ? 'Unresolve' : 'Resolve' }}</button>
|
|
819
|
+
</div>
|
|
820
|
+
}
|
|
821
|
+
</div>
|
|
822
|
+
</ng-template>
|
|
823
|
+
|
|
824
|
+
<ng-template #draftTpl let-l>
|
|
825
|
+
<div class="o-thread"><div class="o-cb o-col" style="gap:8px;padding:10px 12px">
|
|
826
|
+
<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>
|
|
827
|
+
<div class="o-flex o-wrap"><span class="o-faint o-xs">Markdown works.</span>
|
|
828
|
+
<button type="button" class="o-btn o-sm o-r" (click)="drafting.set('')">Cancel</button>
|
|
829
|
+
<button type="button" class="o-btn o-sm" [disabled]="!draftBody().trim()" (click)="send(l, false)" data-testid="line-comment-add">Add single comment</button>
|
|
830
|
+
<button type="button" class="o-btn o-sm o-pri" [disabled]="!draftBody().trim()" (click)="send(l, true)" data-testid="line-comment-review">{{ reviewing() ? 'Add review comment' : 'Start a review' }}</button>
|
|
831
|
+
</div>
|
|
832
|
+
</div></div>
|
|
833
|
+
</ng-template>
|
|
834
|
+
`, 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}.split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.split .side-l{border-right:1px solid var(--o-line-soft)}.split .full{grid-column:1 / -1}.split .o-ln .o-t{white-space:pre-wrap;word-break:break-all}.empty-side{background:var(--o-well)}.viewed{display:inline-flex;align-items:center;gap:6px}\n"] }]
|
|
835
|
+
}], 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 }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], ignoreSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "ignoreSpace", required: false }] }], viewed: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewed", required: false }] }], reviewing: [{ type: i0.Input, args: [{ isSignal: true, alias: "reviewing", required: false }] }], comment: [{ type: i0.Output, args: ["comment"] }], reply: [{ type: i0.Output, args: ["reply"] }], resolve: [{ type: i0.Output, args: ["resolve"] }], discard: [{ type: i0.Output, args: ["discard"] }], viewedChange: [{ type: i0.Output, args: ["viewedChange"] }] } });
|
|
836
|
+
/** Pairs a hunk's lines side by side: context on both sides, a run of removals beside the run of additions that follows it. */
|
|
837
|
+
function pair(lines) {
|
|
838
|
+
const rows = [];
|
|
839
|
+
let i = 0;
|
|
840
|
+
while (i < lines.length) {
|
|
841
|
+
const l = lines[i];
|
|
842
|
+
if (l.kind === 'context') {
|
|
843
|
+
rows.push({ left: l, right: l });
|
|
844
|
+
i++;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const dels = [];
|
|
848
|
+
const adds = [];
|
|
849
|
+
while (i < lines.length && lines[i].kind === 'delete')
|
|
850
|
+
dels.push(lines[i++]);
|
|
851
|
+
while (i < lines.length && lines[i].kind === 'add')
|
|
852
|
+
adds.push(lines[i++]);
|
|
853
|
+
for (let k = 0; k < Math.max(dels.length, adds.length); k++)
|
|
854
|
+
rows.push({ left: dels[k], right: adds[k] });
|
|
855
|
+
}
|
|
856
|
+
return rows;
|
|
857
|
+
}
|
|
613
858
|
|
|
614
859
|
// /{org}/{project}/code (prototype "code_repos"): the project's repositories.
|
|
615
860
|
class ReposPage {
|
|
@@ -1143,7 +1388,7 @@ class RepoHomePage extends RepoPage {
|
|
|
1143
1388
|
</div>
|
|
1144
1389
|
<div class="o-col" style="gap:14px">
|
|
1145
1390
|
<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"><
|
|
1391
|
+
<div class="o-card o-cb o-col"><b>Releases</b><a class="o-sm" [routerLink]="link('releases')" data-testid="repo-releases">Releases and tags</a></div>
|
|
1147
1392
|
<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
1393
|
@if (langs().length) {
|
|
1149
1394
|
<div class="o-card o-cb o-col">
|
|
@@ -1263,7 +1508,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1263
1508
|
</div>
|
|
1264
1509
|
<div class="o-col" style="gap:14px">
|
|
1265
1510
|
<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"><
|
|
1511
|
+
<div class="o-card o-cb o-col"><b>Releases</b><a class="o-sm" [routerLink]="link('releases')" data-testid="repo-releases">Releases and tags</a></div>
|
|
1267
1512
|
<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
1513
|
@if (langs().length) {
|
|
1269
1514
|
<div class="o-card o-cb o-col">
|
|
@@ -1686,7 +1931,7 @@ class CommitPage extends RepoPage {
|
|
|
1686
1931
|
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
1687
1932
|
}
|
|
1688
1933
|
</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 }); }
|
|
1934
|
+
`, 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", "mode", "ignoreSpace", "viewed", "reviewing"], outputs: ["comment", "reply", "resolve", "discard", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1690
1935
|
}
|
|
1691
1936
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitPage, decorators: [{
|
|
1692
1937
|
type: Component,
|
|
@@ -2176,6 +2421,17 @@ class PullPage extends RepoPage {
|
|
|
2176
2421
|
this.initials = initials;
|
|
2177
2422
|
this.dot = checkDot;
|
|
2178
2423
|
this.threads = computed(() => this.timeline().threads, ...(ngDevMode ? [{ debugName: "threads" }] : /* istanbul ignore next */ []));
|
|
2424
|
+
this.mode = signal(readPref('origine.diff.mode') === 'split' ? 'split' : 'unified', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
2425
|
+
this.hideWs = signal(false, ...(ngDevMode ? [{ debugName: "hideWs" }] : /* istanbul ignore next */ []));
|
|
2426
|
+
/** Files marked viewed, kept per pull request head in this browser (a new push resets them). */
|
|
2427
|
+
this.viewed = signal({}, ...(ngDevMode ? [{ debugName: "viewed" }] : /* istanbul ignore next */ []));
|
|
2428
|
+
this.viewedCount = computed(() => (this.cmp()?.files ?? []).filter((f) => this.viewed()[f.path]).length, ...(ngDevMode ? [{ debugName: "viewedCount" }] : /* istanbul ignore next */ []));
|
|
2429
|
+
/** The caller's pending review comments (shown only to them until the review is submitted). */
|
|
2430
|
+
this.myPending = computed(() => {
|
|
2431
|
+
const me = this.host.meId();
|
|
2432
|
+
const t = this.timeline();
|
|
2433
|
+
return [...t.comments, ...t.threads.flatMap((th) => th.comments)].filter((c, i, all) => c.pending && c.authorId === me && all.findIndex((x) => x.id === c.id) === i);
|
|
2434
|
+
}, ...(ngDevMode ? [{ debugName: "myPending" }] : /* istanbul ignore next */ []));
|
|
2179
2435
|
this.openThreads = computed(() => this.threads().filter((t) => !t.resolved && !t.outdated).length, ...(ngDevMode ? [{ debugName: "openThreads" }] : /* istanbul ignore next */ []));
|
|
2180
2436
|
this.tabs = computed(() => {
|
|
2181
2437
|
const p = this.pr();
|
|
@@ -2194,8 +2450,12 @@ class PullPage extends RepoPage {
|
|
|
2194
2450
|
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
2451
|
this.reviewKinds = computed(() => {
|
|
2196
2452
|
const need = this.pr()?.gate.rows.find((r) => /required by branch protection|requires/.test(r.text + (r.meta ?? '')));
|
|
2453
|
+
const comment = { id: 'commented', label: 'Comment', hint: 'General feedback without approving.' };
|
|
2454
|
+
// the author (or a role that cannot push) still posts their pending comments as a review
|
|
2455
|
+
if (!this.pr()?.canReview)
|
|
2456
|
+
return [{ ...comment, hint: 'Post your pending comments.' }];
|
|
2197
2457
|
return [
|
|
2198
|
-
|
|
2458
|
+
comment,
|
|
2199
2459
|
{ id: 'approved', label: 'Approve', hint: need ? `Counts toward the approvals ${this.pr()?.target} requires.` : 'Approve these changes.' },
|
|
2200
2460
|
{ id: 'changes_requested', label: 'Request changes', hint: 'Blocks merge until you approve.' },
|
|
2201
2461
|
];
|
|
@@ -2271,7 +2531,32 @@ class PullPage extends RepoPage {
|
|
|
2271
2531
|
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body }), 'Could not add the comment.', () => this.draft.set(''));
|
|
2272
2532
|
}
|
|
2273
2533
|
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.')
|
|
2534
|
+
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body: c.body, path: c.path, side: c.side, line: c.line, pending: c.pending }), 'Could not add the comment.', () => { if (c.pending)
|
|
2535
|
+
this.note.set('Added to your review. Submit it from Review changes to post your comments.'); });
|
|
2536
|
+
}
|
|
2537
|
+
discard(id) {
|
|
2538
|
+
this.run(this.api.discardComment(this.projectId(), this.name(), this.number, id), 'Could not discard the comment.');
|
|
2539
|
+
}
|
|
2540
|
+
setMode(m) {
|
|
2541
|
+
this.mode.set(m);
|
|
2542
|
+
writePref('origine.diff.mode', m);
|
|
2543
|
+
}
|
|
2544
|
+
viewedKey() {
|
|
2545
|
+
return `origine.viewed.${this.projectId()}.${this.name()}.${this.number}.${this.pr()?.headSha ?? ''}`;
|
|
2546
|
+
}
|
|
2547
|
+
isViewed(path) {
|
|
2548
|
+
return !!this.viewed()[path];
|
|
2549
|
+
}
|
|
2550
|
+
setViewed(path, on) {
|
|
2551
|
+
const v = { ...this.viewed(), [path]: on };
|
|
2552
|
+
if (!on)
|
|
2553
|
+
delete v[path];
|
|
2554
|
+
this.viewed.set(v);
|
|
2555
|
+
writePref(this.viewedKey(), JSON.stringify(v));
|
|
2556
|
+
}
|
|
2557
|
+
jump(path) {
|
|
2558
|
+
const el = this.el.nativeElement.querySelector(`[id="file-${CSS.escape(path)}"]`);
|
|
2559
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
2275
2560
|
}
|
|
2276
2561
|
reply(r) {
|
|
2277
2562
|
this.run(this.api.comment(this.projectId(), this.name(), this.number, { body: r.body, threadId: r.threadId }), 'Could not reply.');
|
|
@@ -2310,7 +2595,16 @@ class PullPage extends RepoPage {
|
|
|
2310
2595
|
const project = this.projectId();
|
|
2311
2596
|
this.api.pull(project, this.name(), this.number).subscribe({
|
|
2312
2597
|
next: (p) => {
|
|
2598
|
+
const headChanged = this.pr()?.headSha !== p.headSha;
|
|
2313
2599
|
this.pr.set(p);
|
|
2600
|
+
if (headChanged) {
|
|
2601
|
+
try {
|
|
2602
|
+
this.viewed.set(JSON.parse(readPref(this.viewedKey()) || '{}'));
|
|
2603
|
+
}
|
|
2604
|
+
catch {
|
|
2605
|
+
this.viewed.set({});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2314
2608
|
if (first) {
|
|
2315
2609
|
this.method.set(p.mergeMethods[0] ?? 'merge');
|
|
2316
2610
|
this.delBranch.set(p.deleteBranchOnMerge);
|
|
@@ -2341,17 +2635,18 @@ class PullPage extends RepoPage {
|
|
|
2341
2635
|
<div class="o-tabs" role="tablist">
|
|
2342
2636
|
@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
2637
|
<span class="o-r"></span>
|
|
2344
|
-
@if (p.state === 'open' && p.canReview) {
|
|
2638
|
+
@if (p.state === 'open' && (p.canReview || myPending().length)) {
|
|
2345
2639
|
<div class="anchor">
|
|
2346
2640
|
<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
2641
|
@if (reviewOpen()) {
|
|
2348
2642
|
<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
2643
|
<b>Finish your review</b>
|
|
2644
|
+
<span class="o-faint o-sm" data-testid="review-pending-note">{{ myPending().length ? myPending().length + ' pending ' + (myPending().length === 1 ? 'comment' : 'comments') + ' included' : 'No pending comments' }}</span>
|
|
2350
2645
|
<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
2646
|
@for (k of reviewKinds(); track k.id) {
|
|
2352
2647
|
<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
2648
|
}
|
|
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>
|
|
2649
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm o-pri o-r" (click)="submitReview()" [disabled]="busy() || (revKind() !== 'approved' && !revBody().trim() && !(revKind() === 'commented' && myPending().length))" data-testid="review-submit">Submit review</button></div>
|
|
2355
2650
|
</div>
|
|
2356
2651
|
}
|
|
2357
2652
|
</div>
|
|
@@ -2435,10 +2730,28 @@ class PullPage extends RepoPage {
|
|
|
2435
2730
|
</div>
|
|
2436
2731
|
}
|
|
2437
2732
|
@case ('files') {
|
|
2438
|
-
<div class="o-flex o-wrap"
|
|
2439
|
-
|
|
2440
|
-
<
|
|
2441
|
-
|
|
2733
|
+
<div class="o-flex o-wrap">
|
|
2734
|
+
<div class="o-seg" role="group" aria-label="Diff view"><button type="button" [class.o-on]="mode() === 'unified'" [attr.aria-pressed]="mode() === 'unified'" (click)="setMode('unified')">Unified</button><button type="button" [class.o-on]="mode() === 'split'" [attr.aria-pressed]="mode() === 'split'" (click)="setMode('split')" data-testid="diff-split">Split</button></div>
|
|
2735
|
+
<button type="button" class="o-btn o-sm" [class.o-on]="hideWs()" [attr.aria-pressed]="hideWs()" (click)="hideWs.set(!hideWs())" data-testid="hide-ws">Hide whitespace</button>
|
|
2736
|
+
@if (myPending().length) {<span class="o-chip o-amb" data-testid="pending-count">{{ myPending().length }} pending {{ myPending().length === 1 ? 'comment' : 'comments' }}</span>}
|
|
2737
|
+
<span class="o-r o-faint o-sm" data-testid="viewed-note">{{ viewedCount() }} of {{ cmp()?.files?.length ?? 0 }} files viewed · {{ openThreads() }} unresolved {{ openThreads() === 1 ? 'conversation' : 'conversations' }}</span>
|
|
2738
|
+
</div>
|
|
2739
|
+
<div class="files-split">
|
|
2740
|
+
@if ((cmp()?.files?.length ?? 0) > 1) {
|
|
2741
|
+
<div class="o-card files-tree" role="navigation" aria-label="Files">
|
|
2742
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2743
|
+
<button type="button" class="o-row o-click o-sm" style="width:100%;border:none;background:none;text-align:left" (click)="jump(f.path)"><span class="o-cbx" [class.o-on]="isViewed(f.path)" aria-hidden="true"></span><span class="o-mono o-ell" style="flex:1">{{ f.path }}</span><span class="o-grn-t o-xs">+{{ f.additions }}</span></button>
|
|
2744
|
+
}
|
|
2745
|
+
</div>
|
|
2746
|
+
}
|
|
2747
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
2748
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2749
|
+
<o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" [mode]="mode()" [ignoreSpace]="hideWs()"
|
|
2750
|
+
[viewed]="isViewed(f.path)" [reviewing]="myPending().length > 0" (viewedChange)="setViewed(f.path, $event)"
|
|
2751
|
+
(comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" (discard)="discard($event)" />
|
|
2752
|
+
} @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
|
|
2753
|
+
</div>
|
|
2754
|
+
</div>
|
|
2442
2755
|
}
|
|
2443
2756
|
@case ('checks') {
|
|
2444
2757
|
<div class="o-card">
|
|
@@ -2471,7 +2784,7 @@ class PullPage extends RepoPage {
|
|
|
2471
2784
|
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
2472
2785
|
}
|
|
2473
2786
|
</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 }); }
|
|
2787
|
+
`, 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}.files-split{display:grid;grid-template-columns:210px minmax(0,1fr);gap:14px;align-items:start}.files-split:has(>:only-child){grid-template-columns:minmax(0,1fr)}.files-tree{position:sticky;top:8px;max-height:calc(100vh - 120px);overflow:auto}@media(max-width:900px){.files-split{grid-template-columns:minmax(0,1fr)}.files-tree{display:none}}\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", "mode", "ignoreSpace", "viewed", "reviewing"], outputs: ["comment", "reply", "resolve", "discard", "viewedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2475
2788
|
}
|
|
2476
2789
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: PullPage, decorators: [{
|
|
2477
2790
|
type: Component,
|
|
@@ -2492,17 +2805,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2492
2805
|
<div class="o-tabs" role="tablist">
|
|
2493
2806
|
@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
2807
|
<span class="o-r"></span>
|
|
2495
|
-
@if (p.state === 'open' && p.canReview) {
|
|
2808
|
+
@if (p.state === 'open' && (p.canReview || myPending().length)) {
|
|
2496
2809
|
<div class="anchor">
|
|
2497
2810
|
<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
2811
|
@if (reviewOpen()) {
|
|
2499
2812
|
<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
2813
|
<b>Finish your review</b>
|
|
2814
|
+
<span class="o-faint o-sm" data-testid="review-pending-note">{{ myPending().length ? myPending().length + ' pending ' + (myPending().length === 1 ? 'comment' : 'comments') + ' included' : 'No pending comments' }}</span>
|
|
2501
2815
|
<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
2816
|
@for (k of reviewKinds(); track k.id) {
|
|
2503
2817
|
<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
2818
|
}
|
|
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>
|
|
2819
|
+
<div class="o-flex"><button type="button" class="o-btn o-sm o-pri o-r" (click)="submitReview()" [disabled]="busy() || (revKind() !== 'approved' && !revBody().trim() && !(revKind() === 'commented' && myPending().length))" data-testid="review-submit">Submit review</button></div>
|
|
2506
2820
|
</div>
|
|
2507
2821
|
}
|
|
2508
2822
|
</div>
|
|
@@ -2586,10 +2900,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2586
2900
|
</div>
|
|
2587
2901
|
}
|
|
2588
2902
|
@case ('files') {
|
|
2589
|
-
<div class="o-flex o-wrap"
|
|
2590
|
-
|
|
2591
|
-
<
|
|
2592
|
-
|
|
2903
|
+
<div class="o-flex o-wrap">
|
|
2904
|
+
<div class="o-seg" role="group" aria-label="Diff view"><button type="button" [class.o-on]="mode() === 'unified'" [attr.aria-pressed]="mode() === 'unified'" (click)="setMode('unified')">Unified</button><button type="button" [class.o-on]="mode() === 'split'" [attr.aria-pressed]="mode() === 'split'" (click)="setMode('split')" data-testid="diff-split">Split</button></div>
|
|
2905
|
+
<button type="button" class="o-btn o-sm" [class.o-on]="hideWs()" [attr.aria-pressed]="hideWs()" (click)="hideWs.set(!hideWs())" data-testid="hide-ws">Hide whitespace</button>
|
|
2906
|
+
@if (myPending().length) {<span class="o-chip o-amb" data-testid="pending-count">{{ myPending().length }} pending {{ myPending().length === 1 ? 'comment' : 'comments' }}</span>}
|
|
2907
|
+
<span class="o-r o-faint o-sm" data-testid="viewed-note">{{ viewedCount() }} of {{ cmp()?.files?.length ?? 0 }} files viewed · {{ openThreads() }} unresolved {{ openThreads() === 1 ? 'conversation' : 'conversations' }}</span>
|
|
2908
|
+
</div>
|
|
2909
|
+
<div class="files-split">
|
|
2910
|
+
@if ((cmp()?.files?.length ?? 0) > 1) {
|
|
2911
|
+
<div class="o-card files-tree" role="navigation" aria-label="Files">
|
|
2912
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2913
|
+
<button type="button" class="o-row o-click o-sm" style="width:100%;border:none;background:none;text-align:left" (click)="jump(f.path)"><span class="o-cbx" [class.o-on]="isViewed(f.path)" aria-hidden="true"></span><span class="o-mono o-ell" style="flex:1">{{ f.path }}</span><span class="o-grn-t o-xs">+{{ f.additions }}</span></button>
|
|
2914
|
+
}
|
|
2915
|
+
</div>
|
|
2916
|
+
}
|
|
2917
|
+
<div class="o-col" style="gap:14px;min-width:0">
|
|
2918
|
+
@for (f of cmp()?.files ?? []; track f.path) {
|
|
2919
|
+
<o-diff [repo]="name()" [file]="f" [head]="p.headSha" [base]="p.mergeBase" [threads]="threads()" [canComment]="p.state === 'open'" [mode]="mode()" [ignoreSpace]="hideWs()"
|
|
2920
|
+
[viewed]="isViewed(f.path)" [reviewing]="myPending().length > 0" (viewedChange)="setViewed(f.path, $event)"
|
|
2921
|
+
(comment)="lineComment($event)" (reply)="reply($event)" (resolve)="resolve($event)" (discard)="discard($event)" />
|
|
2922
|
+
} @empty {<div class="o-card"><div class="o-empty">{{ cmp() ? 'No changes.' : 'Loading…' }}</div></div>}
|
|
2923
|
+
</div>
|
|
2924
|
+
</div>
|
|
2593
2925
|
}
|
|
2594
2926
|
@case ('checks') {
|
|
2595
2927
|
<div class="o-card">
|
|
@@ -2622,11 +2954,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2622
2954
|
<div class="o-card"><div class="o-empty">Loading…</div></div>
|
|
2623
2955
|
}
|
|
2624
2956
|
</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"] }]
|
|
2957
|
+
`, 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}.files-split{display:grid;grid-template-columns:210px minmax(0,1fr);gap:14px;align-items:start}.files-split:has(>:only-child){grid-template-columns:minmax(0,1fr)}.files-tree{position:sticky;top:8px;max-height:calc(100vh - 120px);overflow:auto}@media(max-width:900px){.files-split{grid-template-columns:minmax(0,1fr)}.files-tree{display:none}}\n"] }]
|
|
2626
2958
|
}], ctorParameters: () => [], propDecorators: { outside: [{
|
|
2627
2959
|
type: HostListener,
|
|
2628
2960
|
args: ['document:click', ['$event']]
|
|
2629
2961
|
}] } });
|
|
2962
|
+
// Per-viewer conveniences (the diff view, files viewed); storage can be missing or blocked.
|
|
2963
|
+
function readPref(key) {
|
|
2964
|
+
try {
|
|
2965
|
+
return localStorage.getItem(key) ?? '';
|
|
2966
|
+
}
|
|
2967
|
+
catch {
|
|
2968
|
+
return '';
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
function writePref(key, value) {
|
|
2972
|
+
try {
|
|
2973
|
+
localStorage.setItem(key, value);
|
|
2974
|
+
}
|
|
2975
|
+
catch {
|
|
2976
|
+
/* private window or blocked storage: the setting lasts for this page only */
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2630
2979
|
|
|
2631
2980
|
const BLANK = { id: '', pattern: '', blockDirectPush: true, blockForcePush: true, blockDeletion: true, requiredApprovals: 1, dismissStaleApprovals: true, requireUpToDate: false, requiredChecks: [] };
|
|
2632
2981
|
// /{org}/{project}/code/{repo}/settings?section= (prototype "code_settings"): general settings and
|
|
@@ -2955,6 +3304,386 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2955
3304
|
}]
|
|
2956
3305
|
}], ctorParameters: () => [] });
|
|
2957
3306
|
|
|
3307
|
+
// /{org}/{project}/code/{repo}/releases (prototype code_tags): releases newest first with their
|
|
3308
|
+
// notes, files and source archives, the latest marked; drafts for those who can push; a release
|
|
3309
|
+
// drafted on a new or existing tag with notes generated from the done issues since the previous
|
|
3310
|
+
// one; and the repository's tags.
|
|
3311
|
+
class ReleasesPage extends RepoPage {
|
|
3312
|
+
constructor() {
|
|
3313
|
+
super(...arguments);
|
|
3314
|
+
this.releases = signal([], ...(ngDevMode ? [{ debugName: "releases" }] : /* istanbul ignore next */ []));
|
|
3315
|
+
this.tags = signal([], ...(ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []));
|
|
3316
|
+
this.loaded = signal(false, ...(ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []));
|
|
3317
|
+
this.tagging = signal(false, ...(ngDevMode ? [{ debugName: "tagging" }] : /* istanbul ignore next */ []));
|
|
3318
|
+
this.draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
|
|
3319
|
+
this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
3320
|
+
this.confirming = signal('', ...(ngDevMode ? [{ debugName: "confirming" }] : /* istanbul ignore next */ []));
|
|
3321
|
+
this.freeTags = computed(() => this.tags().filter((t) => !this.used(t.name)), ...(ngDevMode ? [{ debugName: "freeTags" }] : /* istanbul ignore next */ []));
|
|
3322
|
+
this.ago = (s) => ago(s);
|
|
3323
|
+
this.tagName = '';
|
|
3324
|
+
this.tagFrom = '';
|
|
3325
|
+
this.alsoTag = false;
|
|
3326
|
+
}
|
|
3327
|
+
onRepo(r) {
|
|
3328
|
+
this.tagFrom = r.defaultBranch;
|
|
3329
|
+
this.load();
|
|
3330
|
+
}
|
|
3331
|
+
md(s) {
|
|
3332
|
+
return marked.parse(s || '', { async: false, gfm: true, breaks: true });
|
|
3333
|
+
}
|
|
3334
|
+
used(tag) {
|
|
3335
|
+
return this.releases().some((r) => r.tag === tag);
|
|
3336
|
+
}
|
|
3337
|
+
load() {
|
|
3338
|
+
const r = this.repo();
|
|
3339
|
+
if (!r)
|
|
3340
|
+
return;
|
|
3341
|
+
forkJoin({ rel: this.api.releases(this.projectId(), r.name), tags: this.api.refs(this.projectId(), r.name, 'tag') }).subscribe({
|
|
3342
|
+
next: ({ rel, tags }) => { this.releases.set(rel); this.tags.set(sortTags(tags)); this.loaded.set(true); },
|
|
3343
|
+
error: (e) => { this.loaded.set(true); this.error.set(apiMessage(e, 'Could not load the releases.')); },
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
run(o, fallback, done) {
|
|
3347
|
+
this.busy.set(true);
|
|
3348
|
+
this.error.set('');
|
|
3349
|
+
this.note.set('');
|
|
3350
|
+
o.subscribe({
|
|
3351
|
+
next: () => { this.busy.set(false); done?.(); this.load(); },
|
|
3352
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, fallback)); },
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
createTag() {
|
|
3356
|
+
const r = this.repo();
|
|
3357
|
+
const t = this.tagName.trim();
|
|
3358
|
+
if (!r || !t)
|
|
3359
|
+
return;
|
|
3360
|
+
this.run(this.api.createTag(this.projectId(), r.name, t, this.tagFrom.trim() || r.defaultBranch), 'Could not create the tag.', () => {
|
|
3361
|
+
this.tagging.set(false);
|
|
3362
|
+
this.note.set(`Tag ${t} created.`);
|
|
3363
|
+
this.tagName = '';
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
removeTag(t) {
|
|
3367
|
+
const r = this.repo();
|
|
3368
|
+
if (!r)
|
|
3369
|
+
return;
|
|
3370
|
+
this.run(this.api.deleteTag(this.projectId(), r.name, t.name), `Could not delete ${t.name}.`, () => this.note.set(`Tag ${t.name} deleted.`));
|
|
3371
|
+
}
|
|
3372
|
+
startRelease() {
|
|
3373
|
+
const r = this.repo();
|
|
3374
|
+
this.tagging.set(false);
|
|
3375
|
+
this.draft.set({ tag: '', newTag: true, target: r?.defaultBranch ?? 'main', title: '', notes: '', prerelease: false, draft: true });
|
|
3376
|
+
}
|
|
3377
|
+
setTag(d, tag) {
|
|
3378
|
+
d.tag = tag.trim();
|
|
3379
|
+
d.newTag = !this.tags().some((t) => t.name === d.tag);
|
|
3380
|
+
}
|
|
3381
|
+
generate(d) {
|
|
3382
|
+
const r = this.repo();
|
|
3383
|
+
if (!r)
|
|
3384
|
+
return;
|
|
3385
|
+
this.busy.set(true);
|
|
3386
|
+
const target = d.newTag ? d.target || r.defaultBranch : 'refs/tags/' + d.tag;
|
|
3387
|
+
this.api.releaseNotes(this.projectId(), r.name, target).subscribe({
|
|
3388
|
+
next: (n) => { this.busy.set(false); d.notes = n; this.draft.set({ ...d }); },
|
|
3389
|
+
error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not generate the notes.')); },
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
edit(r) {
|
|
3393
|
+
this.tagging.set(false);
|
|
3394
|
+
this.draft.set({ id: r.id, version: r.version, tag: r.tag, newTag: false, target: '', title: r.title === r.tag ? '' : r.title, notes: r.notes, prerelease: r.prerelease, draft: r.draft });
|
|
3395
|
+
}
|
|
3396
|
+
save(d, asDraft) {
|
|
3397
|
+
const r = this.repo();
|
|
3398
|
+
if (!r)
|
|
3399
|
+
return;
|
|
3400
|
+
if (d.id) {
|
|
3401
|
+
this.run(this.api.updateRelease(this.projectId(), r.name, d.id, { title: d.title, notes: d.notes, prerelease: d.prerelease, draft: d.draft && asDraft, version: d.version }), 'Could not save the release.', () => { this.draft.set(null); this.note.set(asDraft ? 'Draft saved.' : `${d.tag} saved.`); });
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
this.run(this.api.createRelease(this.projectId(), r.name, { tag: d.tag, target: d.newTag ? d.target || r.defaultBranch : undefined, title: d.title, notes: d.notes, prerelease: d.prerelease, draft: asDraft }), 'Could not create the release.', () => { this.draft.set(null); this.note.set(asDraft ? `Draft ${d.tag} saved.` : `${d.tag} published.`); });
|
|
3405
|
+
}
|
|
3406
|
+
remove(rel) {
|
|
3407
|
+
const r = this.repo();
|
|
3408
|
+
if (!r)
|
|
3409
|
+
return;
|
|
3410
|
+
this.confirming.set('');
|
|
3411
|
+
this.run(this.api.deleteRelease(this.projectId(), r.name, rel.id, this.alsoTag), `Could not delete ${rel.tag}.`, () => this.note.set(`Release ${rel.tag} deleted${this.alsoTag ? ' with its tag' : ''}.`));
|
|
3412
|
+
}
|
|
3413
|
+
upload(rel, ev) {
|
|
3414
|
+
const r = this.repo();
|
|
3415
|
+
const input = ev.target;
|
|
3416
|
+
const file = input.files?.[0];
|
|
3417
|
+
if (!r || !file)
|
|
3418
|
+
return;
|
|
3419
|
+
if (file.size > 25 * 1024 * 1024) {
|
|
3420
|
+
this.error.set('A release file is at most 25 MB.');
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3423
|
+
this.run(this.api.addAsset(this.projectId(), r.name, rel.id, file), 'Could not add the file.', () => { input.value = ''; this.note.set(`${file.name} added to ${rel.tag}.`); });
|
|
3424
|
+
}
|
|
3425
|
+
removeAsset(rel, id) {
|
|
3426
|
+
const r = this.repo();
|
|
3427
|
+
if (!r)
|
|
3428
|
+
return;
|
|
3429
|
+
this.run(this.api.deleteAsset(this.projectId(), r.name, rel.id, id), 'Could not remove the file.');
|
|
3430
|
+
}
|
|
3431
|
+
download(rel, id, filename) {
|
|
3432
|
+
const r = this.repo();
|
|
3433
|
+
if (!r)
|
|
3434
|
+
return;
|
|
3435
|
+
this.api.assetBlob(this.projectId(), r.name, rel.id, id).subscribe({
|
|
3436
|
+
next: (b) => this.save_(b, filename),
|
|
3437
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not download the file.')),
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
archive(rel, format) {
|
|
3441
|
+
const r = this.repo();
|
|
3442
|
+
if (!r)
|
|
3443
|
+
return;
|
|
3444
|
+
this.api.archiveUrl(this.projectId(), r.name, 'refs/tags/' + rel.tag, format).subscribe({
|
|
3445
|
+
next: (url) => { if (url)
|
|
3446
|
+
window.location.assign(url); },
|
|
3447
|
+
error: (e) => this.error.set(apiMessage(e, 'Could not prepare the archive.')),
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
save_(b, filename) {
|
|
3451
|
+
const url = URL.createObjectURL(b);
|
|
3452
|
+
const link = document.createElement('a');
|
|
3453
|
+
link.href = url;
|
|
3454
|
+
link.download = filename;
|
|
3455
|
+
link.click();
|
|
3456
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
3457
|
+
}
|
|
3458
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ReleasesPage, deps: null, target: i0.ɵɵFactoryTarget.Component }); }
|
|
3459
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: ReleasesPage, isStandalone: true, selector: "o-code-releases", usesInheritance: true, ngImport: i0, template: `
|
|
3460
|
+
<div class="o-page o-narrow">
|
|
3461
|
+
<div class="o-ph">
|
|
3462
|
+
<div class="o-col"><span class="o-eyebrow">{{ name() }}</span><h1 class="o-t">Releases and tags</h1></div>
|
|
3463
|
+
@if (canWrite()) {
|
|
3464
|
+
<div class="o-acts">
|
|
3465
|
+
<button type="button" class="o-btn o-sm" (click)="tagging.set(!tagging()); draft.set(null)" data-testid="tag-new">New tag</button>
|
|
3466
|
+
<button type="button" class="o-btn o-sm o-dark" (click)="startRelease()" data-testid="release-new">Draft release</button>
|
|
3467
|
+
</div>
|
|
3468
|
+
}
|
|
3469
|
+
</div>
|
|
3470
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
3471
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
3472
|
+
|
|
3473
|
+
@if (tagging()) {
|
|
3474
|
+
<form class="o-card" (ngSubmit)="createTag()" data-testid="tag-form">
|
|
3475
|
+
<div class="o-ch"><b>New tag</b></div>
|
|
3476
|
+
<div class="o-cb o-flex o-wrap" style="align-items:flex-end">
|
|
3477
|
+
<div class="o-fld"><label for="tg-name">Tag</label><input id="tg-name" class="o-in o-mono" name="tag" placeholder="v1.8.0" [(ngModel)]="tagName" /></div>
|
|
3478
|
+
<div class="o-fld"><label for="tg-from">At</label><input id="tg-from" class="o-in o-mono" name="from" [(ngModel)]="tagFrom" placeholder="main or a commit" /></div>
|
|
3479
|
+
<button type="button" class="o-btn o-sm o-r" (click)="tagging.set(false)">Cancel</button>
|
|
3480
|
+
<button type="submit" class="o-btn o-sm o-dark" [disabled]="busy() || !tagName.trim()">Create tag</button>
|
|
3481
|
+
</div>
|
|
3482
|
+
</form>
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
@if (draft(); as d) {
|
|
3486
|
+
<div class="o-card" data-testid="release-form">
|
|
3487
|
+
<div class="o-ch"><b>{{ d.id ? 'Edit ' + d.tag : 'Draft a release' }}</b></div>
|
|
3488
|
+
<div class="o-cb o-col" style="gap:12px">
|
|
3489
|
+
@if (!d.id) {
|
|
3490
|
+
<div class="o-flex o-wrap" style="align-items:flex-end">
|
|
3491
|
+
<div class="o-fld"><label for="rl-tag">Tag</label>
|
|
3492
|
+
<input id="rl-tag" class="o-in o-mono" list="rl-tags" placeholder="v1.8.0" [ngModel]="d.tag" (ngModelChange)="setTag(d, $event)" data-testid="release-tag" />
|
|
3493
|
+
<datalist id="rl-tags">@for (t of freeTags(); track t.name) {<option [value]="t.name"></option>}</datalist>
|
|
3494
|
+
</div>
|
|
3495
|
+
@if (d.newTag) {<div class="o-fld"><label for="rl-target">New tag at</label><input id="rl-target" class="o-in o-mono" [(ngModel)]="d.target" placeholder="main" /></div>}
|
|
3496
|
+
<span class="o-hint" style="padding-bottom:8px">{{ d.tag ? (d.newTag ? 'A new tag, made when you save.' : 'An existing tag.') : '' }}</span>
|
|
3497
|
+
</div>
|
|
3498
|
+
}
|
|
3499
|
+
<div class="o-fld"><label for="rl-title">Title</label><input id="rl-title" class="o-in" maxlength="255" [(ngModel)]="d.title" placeholder="Defaults to the tag" data-testid="release-title" /></div>
|
|
3500
|
+
<div class="o-fld">
|
|
3501
|
+
<div class="o-flex"><label for="rl-notes">Notes</label>
|
|
3502
|
+
@if (!d.id) {<button type="button" class="o-lnk o-r o-sm" [disabled]="busy() || !d.tag" (click)="generate(d)" data-testid="release-generate">Generate from done issues</button>}
|
|
3503
|
+
</div>
|
|
3504
|
+
<textarea id="rl-notes" class="o-in" rows="7" style="height:auto;padding:8px 10px" [(ngModel)]="d.notes" placeholder="Markdown works." data-testid="release-notes"></textarea>
|
|
3505
|
+
<span class="o-hint">Generated notes list the done issues and merged pull requests since the previous release.</span>
|
|
3506
|
+
</div>
|
|
3507
|
+
<label class="o-flex o-sm"><input type="checkbox" [(ngModel)]="d.prerelease" /> Pre-release (never marked Latest)</label>
|
|
3508
|
+
</div>
|
|
3509
|
+
<div class="o-cf">
|
|
3510
|
+
<button type="button" class="o-btn o-sm o-r" (click)="draft.set(null)">Cancel</button>
|
|
3511
|
+
@if (!d.id || d.draft) {<button type="button" class="o-btn o-sm" [disabled]="busy() || !d.tag" (click)="save(d, true)" data-testid="release-save-draft">Save draft</button>}
|
|
3512
|
+
<button type="button" class="o-btn o-sm o-dark" [disabled]="busy() || !d.tag" (click)="save(d, false)" data-testid="release-publish">{{ d.id && !d.draft ? 'Save' : 'Publish release' }}</button>
|
|
3513
|
+
</div>
|
|
3514
|
+
</div>
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
@for (r of releases(); track r.id) {
|
|
3518
|
+
<div class="o-card rel" [attr.data-testid]="'release-' + r.tag">
|
|
3519
|
+
<div class="o-ch">
|
|
3520
|
+
<b class="o-md">{{ r.tag }}</b>
|
|
3521
|
+
@if (r.latest) {<span class="o-chip o-grn">Latest</span>}
|
|
3522
|
+
@if (r.draft) {<span class="o-chip o-amb">Draft</span>}
|
|
3523
|
+
@if (r.prerelease) {<span class="o-chip">Pre-release</span>}
|
|
3524
|
+
<span class="o-r o-mono o-sm o-faint">{{ r.short }} · {{ ago(r.publishedAt || r.createdAt) }} · {{ people().name(r.createdBy) }}</span>
|
|
3525
|
+
</div>
|
|
3526
|
+
<div class="o-cb">
|
|
3527
|
+
@if (r.title && r.title !== r.tag) {<h4>{{ r.title }}</h4>}
|
|
3528
|
+
<div class="o-md" style="font-size:13px" [innerHTML]="r.notes ? md(r.notes) : '<p class="o-faint">No notes.</p>'"></div>
|
|
3529
|
+
</div>
|
|
3530
|
+
<div class="o-cf assets">
|
|
3531
|
+
<span>Assets</span>
|
|
3532
|
+
@for (a of r.assets; track a.id) {
|
|
3533
|
+
<button type="button" class="o-lnk o-mono" (click)="download(r, a.id, a.name)" [attr.aria-label]="'Download ' + a.name">{{ a.name }}</button>
|
|
3534
|
+
@if (canWrite()) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeAsset(r, a.id)" [attr.aria-label]="'Remove ' + a.name">×</button>}
|
|
3535
|
+
}
|
|
3536
|
+
<button type="button" class="o-lnk" (click)="archive(r, 'zip')">Source (zip)</button>
|
|
3537
|
+
<button type="button" class="o-lnk" (click)="archive(r, 'tar.gz')">Source (tar.gz)</button>
|
|
3538
|
+
@if (canWrite()) {
|
|
3539
|
+
<span class="o-r o-flex" style="gap:10px">
|
|
3540
|
+
<label class="o-lnk" style="cursor:pointer">Add file<input type="file" class="o-sr-only" (change)="upload(r, $event)" [attr.aria-label]="'Add a file to ' + r.tag" /></label>
|
|
3541
|
+
<button type="button" class="o-lnk" (click)="edit(r)" [attr.aria-label]="'Edit ' + r.tag">Edit</button>
|
|
3542
|
+
@if (confirming() === r.id) {
|
|
3543
|
+
<label class="o-flex o-xs"><input type="checkbox" [(ngModel)]="alsoTag" /> and the tag</label>
|
|
3544
|
+
<button type="button" class="o-lnk o-red-t" (click)="remove(r)">Delete</button><button type="button" class="o-lnk" (click)="confirming.set('')">Keep</button>
|
|
3545
|
+
} @else {
|
|
3546
|
+
<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="confirming.set(r.id); alsoTag = false" [attr.aria-label]="'Delete ' + r.tag">Delete</button>
|
|
3547
|
+
}
|
|
3548
|
+
</span>
|
|
3549
|
+
}
|
|
3550
|
+
</div>
|
|
3551
|
+
</div>
|
|
3552
|
+
} @empty {
|
|
3553
|
+
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (canWrite() ? ' Draft one from a tag; its notes can come from the issues it finishes.' : '') : 'Loading…' }}</div></div>
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
<div class="o-card" data-testid="tags">
|
|
3557
|
+
<div class="o-ch"><b>Tags</b><span class="o-r o-faint o-sm">{{ tags().length }}</span></div>
|
|
3558
|
+
@for (t of tags(); track t.name) {
|
|
3559
|
+
<div class="o-row" [attr.data-testid]="'tag-' + t.name">
|
|
3560
|
+
<span class="o-mono o-sm o-b5">{{ t.name }}</span><span class="o-faint o-sm o-mono">{{ t.commit?.short }}</span>
|
|
3561
|
+
<span class="o-r o-faint o-sm">{{ ago(t.commit?.committer?.when) }}</span>
|
|
3562
|
+
@if (canWrite() && !used(t.name)) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeTag(t)" [attr.aria-label]="'Delete tag ' + t.name">Delete</button>}
|
|
3563
|
+
</div>
|
|
3564
|
+
} @empty {<div class="o-empty o-sm">No tags yet.</div>}
|
|
3565
|
+
</div>
|
|
3566
|
+
</div>
|
|
3567
|
+
`, isInline: true, styles: [".rel h4{margin:0 0 6px;font-size:15px}.rel .o-md ul{margin:0;padding-left:20px}.assets{display:flex;gap:12px;flex-wrap:wrap;align-items:center}\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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { 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 }); }
|
|
3568
|
+
}
|
|
3569
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: ReleasesPage, decorators: [{
|
|
3570
|
+
type: Component,
|
|
3571
|
+
args: [{ selector: 'o-code-releases', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
3572
|
+
<div class="o-page o-narrow">
|
|
3573
|
+
<div class="o-ph">
|
|
3574
|
+
<div class="o-col"><span class="o-eyebrow">{{ name() }}</span><h1 class="o-t">Releases and tags</h1></div>
|
|
3575
|
+
@if (canWrite()) {
|
|
3576
|
+
<div class="o-acts">
|
|
3577
|
+
<button type="button" class="o-btn o-sm" (click)="tagging.set(!tagging()); draft.set(null)" data-testid="tag-new">New tag</button>
|
|
3578
|
+
<button type="button" class="o-btn o-sm o-dark" (click)="startRelease()" data-testid="release-new">Draft release</button>
|
|
3579
|
+
</div>
|
|
3580
|
+
}
|
|
3581
|
+
</div>
|
|
3582
|
+
@if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
|
|
3583
|
+
@if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
|
|
3584
|
+
|
|
3585
|
+
@if (tagging()) {
|
|
3586
|
+
<form class="o-card" (ngSubmit)="createTag()" data-testid="tag-form">
|
|
3587
|
+
<div class="o-ch"><b>New tag</b></div>
|
|
3588
|
+
<div class="o-cb o-flex o-wrap" style="align-items:flex-end">
|
|
3589
|
+
<div class="o-fld"><label for="tg-name">Tag</label><input id="tg-name" class="o-in o-mono" name="tag" placeholder="v1.8.0" [(ngModel)]="tagName" /></div>
|
|
3590
|
+
<div class="o-fld"><label for="tg-from">At</label><input id="tg-from" class="o-in o-mono" name="from" [(ngModel)]="tagFrom" placeholder="main or a commit" /></div>
|
|
3591
|
+
<button type="button" class="o-btn o-sm o-r" (click)="tagging.set(false)">Cancel</button>
|
|
3592
|
+
<button type="submit" class="o-btn o-sm o-dark" [disabled]="busy() || !tagName.trim()">Create tag</button>
|
|
3593
|
+
</div>
|
|
3594
|
+
</form>
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
@if (draft(); as d) {
|
|
3598
|
+
<div class="o-card" data-testid="release-form">
|
|
3599
|
+
<div class="o-ch"><b>{{ d.id ? 'Edit ' + d.tag : 'Draft a release' }}</b></div>
|
|
3600
|
+
<div class="o-cb o-col" style="gap:12px">
|
|
3601
|
+
@if (!d.id) {
|
|
3602
|
+
<div class="o-flex o-wrap" style="align-items:flex-end">
|
|
3603
|
+
<div class="o-fld"><label for="rl-tag">Tag</label>
|
|
3604
|
+
<input id="rl-tag" class="o-in o-mono" list="rl-tags" placeholder="v1.8.0" [ngModel]="d.tag" (ngModelChange)="setTag(d, $event)" data-testid="release-tag" />
|
|
3605
|
+
<datalist id="rl-tags">@for (t of freeTags(); track t.name) {<option [value]="t.name"></option>}</datalist>
|
|
3606
|
+
</div>
|
|
3607
|
+
@if (d.newTag) {<div class="o-fld"><label for="rl-target">New tag at</label><input id="rl-target" class="o-in o-mono" [(ngModel)]="d.target" placeholder="main" /></div>}
|
|
3608
|
+
<span class="o-hint" style="padding-bottom:8px">{{ d.tag ? (d.newTag ? 'A new tag, made when you save.' : 'An existing tag.') : '' }}</span>
|
|
3609
|
+
</div>
|
|
3610
|
+
}
|
|
3611
|
+
<div class="o-fld"><label for="rl-title">Title</label><input id="rl-title" class="o-in" maxlength="255" [(ngModel)]="d.title" placeholder="Defaults to the tag" data-testid="release-title" /></div>
|
|
3612
|
+
<div class="o-fld">
|
|
3613
|
+
<div class="o-flex"><label for="rl-notes">Notes</label>
|
|
3614
|
+
@if (!d.id) {<button type="button" class="o-lnk o-r o-sm" [disabled]="busy() || !d.tag" (click)="generate(d)" data-testid="release-generate">Generate from done issues</button>}
|
|
3615
|
+
</div>
|
|
3616
|
+
<textarea id="rl-notes" class="o-in" rows="7" style="height:auto;padding:8px 10px" [(ngModel)]="d.notes" placeholder="Markdown works." data-testid="release-notes"></textarea>
|
|
3617
|
+
<span class="o-hint">Generated notes list the done issues and merged pull requests since the previous release.</span>
|
|
3618
|
+
</div>
|
|
3619
|
+
<label class="o-flex o-sm"><input type="checkbox" [(ngModel)]="d.prerelease" /> Pre-release (never marked Latest)</label>
|
|
3620
|
+
</div>
|
|
3621
|
+
<div class="o-cf">
|
|
3622
|
+
<button type="button" class="o-btn o-sm o-r" (click)="draft.set(null)">Cancel</button>
|
|
3623
|
+
@if (!d.id || d.draft) {<button type="button" class="o-btn o-sm" [disabled]="busy() || !d.tag" (click)="save(d, true)" data-testid="release-save-draft">Save draft</button>}
|
|
3624
|
+
<button type="button" class="o-btn o-sm o-dark" [disabled]="busy() || !d.tag" (click)="save(d, false)" data-testid="release-publish">{{ d.id && !d.draft ? 'Save' : 'Publish release' }}</button>
|
|
3625
|
+
</div>
|
|
3626
|
+
</div>
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
@for (r of releases(); track r.id) {
|
|
3630
|
+
<div class="o-card rel" [attr.data-testid]="'release-' + r.tag">
|
|
3631
|
+
<div class="o-ch">
|
|
3632
|
+
<b class="o-md">{{ r.tag }}</b>
|
|
3633
|
+
@if (r.latest) {<span class="o-chip o-grn">Latest</span>}
|
|
3634
|
+
@if (r.draft) {<span class="o-chip o-amb">Draft</span>}
|
|
3635
|
+
@if (r.prerelease) {<span class="o-chip">Pre-release</span>}
|
|
3636
|
+
<span class="o-r o-mono o-sm o-faint">{{ r.short }} · {{ ago(r.publishedAt || r.createdAt) }} · {{ people().name(r.createdBy) }}</span>
|
|
3637
|
+
</div>
|
|
3638
|
+
<div class="o-cb">
|
|
3639
|
+
@if (r.title && r.title !== r.tag) {<h4>{{ r.title }}</h4>}
|
|
3640
|
+
<div class="o-md" style="font-size:13px" [innerHTML]="r.notes ? md(r.notes) : '<p class="o-faint">No notes.</p>'"></div>
|
|
3641
|
+
</div>
|
|
3642
|
+
<div class="o-cf assets">
|
|
3643
|
+
<span>Assets</span>
|
|
3644
|
+
@for (a of r.assets; track a.id) {
|
|
3645
|
+
<button type="button" class="o-lnk o-mono" (click)="download(r, a.id, a.name)" [attr.aria-label]="'Download ' + a.name">{{ a.name }}</button>
|
|
3646
|
+
@if (canWrite()) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeAsset(r, a.id)" [attr.aria-label]="'Remove ' + a.name">×</button>}
|
|
3647
|
+
}
|
|
3648
|
+
<button type="button" class="o-lnk" (click)="archive(r, 'zip')">Source (zip)</button>
|
|
3649
|
+
<button type="button" class="o-lnk" (click)="archive(r, 'tar.gz')">Source (tar.gz)</button>
|
|
3650
|
+
@if (canWrite()) {
|
|
3651
|
+
<span class="o-r o-flex" style="gap:10px">
|
|
3652
|
+
<label class="o-lnk" style="cursor:pointer">Add file<input type="file" class="o-sr-only" (change)="upload(r, $event)" [attr.aria-label]="'Add a file to ' + r.tag" /></label>
|
|
3653
|
+
<button type="button" class="o-lnk" (click)="edit(r)" [attr.aria-label]="'Edit ' + r.tag">Edit</button>
|
|
3654
|
+
@if (confirming() === r.id) {
|
|
3655
|
+
<label class="o-flex o-xs"><input type="checkbox" [(ngModel)]="alsoTag" /> and the tag</label>
|
|
3656
|
+
<button type="button" class="o-lnk o-red-t" (click)="remove(r)">Delete</button><button type="button" class="o-lnk" (click)="confirming.set('')">Keep</button>
|
|
3657
|
+
} @else {
|
|
3658
|
+
<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="confirming.set(r.id); alsoTag = false" [attr.aria-label]="'Delete ' + r.tag">Delete</button>
|
|
3659
|
+
}
|
|
3660
|
+
</span>
|
|
3661
|
+
}
|
|
3662
|
+
</div>
|
|
3663
|
+
</div>
|
|
3664
|
+
} @empty {
|
|
3665
|
+
<div class="o-card"><div class="o-empty">{{ loaded() ? 'No releases yet.' + (canWrite() ? ' Draft one from a tag; its notes can come from the issues it finishes.' : '') : 'Loading…' }}</div></div>
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
<div class="o-card" data-testid="tags">
|
|
3669
|
+
<div class="o-ch"><b>Tags</b><span class="o-r o-faint o-sm">{{ tags().length }}</span></div>
|
|
3670
|
+
@for (t of tags(); track t.name) {
|
|
3671
|
+
<div class="o-row" [attr.data-testid]="'tag-' + t.name">
|
|
3672
|
+
<span class="o-mono o-sm o-b5">{{ t.name }}</span><span class="o-faint o-sm o-mono">{{ t.commit?.short }}</span>
|
|
3673
|
+
<span class="o-r o-faint o-sm">{{ ago(t.commit?.committer?.when) }}</span>
|
|
3674
|
+
@if (canWrite() && !used(t.name)) {<button type="button" class="o-lnk" style="color:var(--o-mut)" (click)="removeTag(t)" [attr.aria-label]="'Delete tag ' + t.name">Delete</button>}
|
|
3675
|
+
</div>
|
|
3676
|
+
} @empty {<div class="o-empty o-sm">No tags yet.</div>}
|
|
3677
|
+
</div>
|
|
3678
|
+
</div>
|
|
3679
|
+
`, styles: [".rel h4{margin:0 0 6px;font-size:15px}.rel .o-md ul{margin:0;padding-left:20px}.assets{display:flex;gap:12px;flex-wrap:wrap;align-items:center}\n"] }]
|
|
3680
|
+
}] });
|
|
3681
|
+
/** Newest commit first; tags on commits of the same second by version, highest first (v1.10 after v1.9). */
|
|
3682
|
+
function sortTags(tags) {
|
|
3683
|
+
const when = (t) => Date.parse(t.commit?.committer?.when ?? '') || 0;
|
|
3684
|
+
return [...tags].sort((a, b) => when(b) - when(a) || b.name.localeCompare(a.name, undefined, { numeric: true }));
|
|
3685
|
+
}
|
|
3686
|
+
|
|
2958
3687
|
/*
|
|
2959
3688
|
* @chisquare-tech/origine-ngx-apps-code: the Code module's screens (TRD §6.10). The host app provides
|
|
2960
3689
|
* CodeHost and mounts the pages at /{org}/{project}/code, /code/new, /code/pulls and
|
|
@@ -2965,5 +3694,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2965
3694
|
* Generated bundle index. Do not edit.
|
|
2966
3695
|
*/
|
|
2967
3696
|
|
|
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 };
|
|
3697
|
+
export { BranchesPage, CodeApi, CodeHost, CommitPage, CommitsPage, CurrentRepo, CurrentRepoPage, DiffView, FilePage, NewRepoPage, People, PullPage, PullsPage, RefPicker, ReleasesPage, RepoHomePage, RepoPage, RepoSettingsPage, ReposPage, TOKEN_CSS, ago, apiMessage, checkDot, copy, dayLabel, initials, langColour, pair, shortDate, size };
|
|
2969
3698
|
//# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map
|