@chisquare-tech/origine-ngx-apps-code 0.4.0 → 0.5.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.
@@ -65,8 +65,15 @@ class CodeApi {
65
65
  tree(project, name, ref, path) {
66
66
  return this.api.get(`${this.repoPath(project, name)}/tree`, { ref, path }).pipe(map((r) => data(r)));
67
67
  }
68
- blob(project, name, ref, path) {
69
- return this.api.get(`${this.repoPath(project, name)}/blob`, { ref, path }).pipe(map((r) => data(r)));
68
+ blob(project, name, ref, path, withText = false) {
69
+ const p = { ref, path };
70
+ if (withText)
71
+ p['format'] = 'text';
72
+ return this.api.get(`${this.repoPath(project, name)}/blob`, p).pipe(map((r) => data(r)));
73
+ }
74
+ /** A commit from the browser (DEV-13): files, base64 uploads and deletes; from a new branch it can open a pull request. */
75
+ commitFiles(project, name, body) {
76
+ return this.api.post(`${this.repoPath(project, name)}/commits`, body).pipe(map((r) => data(r)));
70
77
  }
71
78
  blame(project, name, ref, path) {
72
79
  return this.api.get(`${this.repoPath(project, name)}/blame`, { ref, path }).pipe(map((r) => data(r) ?? []));
@@ -1226,6 +1233,7 @@ class RepoHomePage extends RepoPage {
1226
1233
  this.path = signal('', ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ []));
1227
1234
  this.proto = signal('https', ...(ngDevMode ? [{ debugName: "proto" }] : /* istanbul ignore next */ []));
1228
1235
  this.cloneOpen = signal(false, ...(ngDevMode ? [{ debugName: "cloneOpen" }] : /* istanbul ignore next */ []));
1236
+ this.addOpen = signal(false, ...(ngDevMode ? [{ debugName: "addOpen" }] : /* istanbul ignore next */ []));
1229
1237
  this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
1230
1238
  this.finding = signal(false, ...(ngDevMode ? [{ debugName: "finding" }] : /* istanbul ignore next */ []));
1231
1239
  this.fq = signal('', ...(ngDevMode ? [{ debugName: "fq" }] : /* istanbul ignore next */ []));
@@ -1354,6 +1362,17 @@ class RepoHomePage extends RepoPage {
1354
1362
  </nav>
1355
1363
  <span class="o-r"></span>
1356
1364
  <button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
1365
+ @if (canWrite()) {
1366
+ <span class="anchor add">
1367
+ <button type="button" class="o-btn o-sm" (click)="addOpen.set(!addOpen())" [attr.aria-expanded]="addOpen()" aria-haspopup="menu" data-testid="add-file">Add file ⌄</button>
1368
+ @if (addOpen()) {
1369
+ <div class="o-menu" role="menu" style="right:0;top:34px;width:200px">
1370
+ <a role="menuitem" [routerLink]="link('new')" [queryParams]="{ ref: ref() || null, path: path() || null }" data-testid="add-new">Create new file</a>
1371
+ <a role="menuitem" [routerLink]="link('upload')" [queryParams]="{ ref: ref() || null, path: path() || null }" data-testid="add-upload">Upload files</a>
1372
+ </div>
1373
+ }
1374
+ </span>
1375
+ }
1357
1376
  <span class="anchor">
1358
1377
  <button type="button" class="o-btn o-sm o-pri" (click)="cloneOpen.set(!cloneOpen())" [attr.aria-expanded]="cloneOpen()" aria-haspopup="dialog" data-testid="clone-menu">Code ⌄</button>
1359
1378
  @if (cloneOpen()) {
@@ -1474,6 +1493,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1474
1493
  </nav>
1475
1494
  <span class="o-r"></span>
1476
1495
  <button type="button" class="o-btn o-sm" (click)="openFinder()">Go to file <span class="o-kbd">T</span></button>
1496
+ @if (canWrite()) {
1497
+ <span class="anchor add">
1498
+ <button type="button" class="o-btn o-sm" (click)="addOpen.set(!addOpen())" [attr.aria-expanded]="addOpen()" aria-haspopup="menu" data-testid="add-file">Add file ⌄</button>
1499
+ @if (addOpen()) {
1500
+ <div class="o-menu" role="menu" style="right:0;top:34px;width:200px">
1501
+ <a role="menuitem" [routerLink]="link('new')" [queryParams]="{ ref: ref() || null, path: path() || null }" data-testid="add-new">Create new file</a>
1502
+ <a role="menuitem" [routerLink]="link('upload')" [queryParams]="{ ref: ref() || null, path: path() || null }" data-testid="add-upload">Upload files</a>
1503
+ </div>
1504
+ }
1505
+ </span>
1506
+ }
1477
1507
  <span class="anchor">
1478
1508
  <button type="button" class="o-btn o-sm o-pri" (click)="cloneOpen.set(!cloneOpen())" [attr.aria-expanded]="cloneOpen()" aria-haspopup="dialog" data-testid="clone-menu">Code ⌄</button>
1479
1509
  @if (cloneOpen()) {
@@ -1557,6 +1587,109 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1557
1587
  args: ['document:keydown', ['$event']]
1558
1588
  }] } });
1559
1589
 
1590
+ /** Whether a branch protection pattern names the branch (as code's protect.Match: * within a segment, ** across). */
1591
+ function matchesPattern(pattern, branch) {
1592
+ const seg = (p) => '^' + p.split('*').map((x) => x.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*') + '$';
1593
+ const pat = pattern.split('/');
1594
+ const name = branch.split('/');
1595
+ const go = (i, j) => {
1596
+ if (i === pat.length)
1597
+ return j === name.length;
1598
+ if (pat[i] === '**') {
1599
+ if (i === pat.length - 1)
1600
+ return j < name.length;
1601
+ for (let k = j; k <= name.length; k++)
1602
+ if (go(i + 1, k))
1603
+ return true;
1604
+ return false;
1605
+ }
1606
+ return j < name.length && new RegExp(seg(pat[i])).test(name[j]) && go(i + 1, j + 1);
1607
+ };
1608
+ return go(0, 0);
1609
+ }
1610
+ // The web editor's commit box (prototype code_file "Commit and open PR", DEV-13): a message, and
1611
+ // either the branch itself or a new branch with a pull request. A branch whose rules block direct
1612
+ // pushes only offers the second.
1613
+ class CommitBox {
1614
+ constructor() {
1615
+ this.api = inject(CodeApi);
1616
+ this.host = inject(CodeHost);
1617
+ this.repo = input.required(...(ngDevMode ? [{ debugName: "repo" }] : /* istanbul ignore next */ []));
1618
+ this.branch = input.required(...(ngDevMode ? [{ debugName: "branch" }] : /* istanbul ignore next */ []));
1619
+ this.defaultMessage = input('', ...(ngDevMode ? [{ debugName: "defaultMessage" }] : /* istanbul ignore next */ []));
1620
+ this.busy = input(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1621
+ this.commit = output();
1622
+ this.cancel = output();
1623
+ this.message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
1624
+ this.toNew = signal(false, ...(ngDevMode ? [{ debugName: "toNew" }] : /* istanbul ignore next */ []));
1625
+ this.newBranch = signal('', ...(ngDevMode ? [{ debugName: "newBranch" }] : /* istanbul ignore next */ []));
1626
+ this.rules = signal([], ...(ngDevMode ? [{ debugName: "rules" }] : /* istanbul ignore next */ []));
1627
+ this.blocked = computed(() => this.rules().some((r) => r.blockDirectPush && matchesPattern(r.pattern, this.branch())), ...(ngDevMode ? [{ debugName: "blocked" }] : /* istanbul ignore next */ []));
1628
+ effect(() => {
1629
+ const m = this.defaultMessage();
1630
+ untracked(() => this.message.set(m));
1631
+ });
1632
+ effect(() => {
1633
+ const repo = this.repo();
1634
+ const p = this.host.project();
1635
+ if (!p || !repo)
1636
+ return;
1637
+ untracked(() => {
1638
+ this.api.protections(p.id, repo).subscribe({ next: (r) => this.rules.set(r), error: () => undefined });
1639
+ const me = (this.host.people().find((x) => x.id === this.host.meId())?.name || 'patch').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'patch';
1640
+ this.newBranch.set(`${me}/patch-${Math.floor(Date.now() / 1000) % 100000}`);
1641
+ });
1642
+ });
1643
+ }
1644
+ send() {
1645
+ const toNew = this.toNew() || this.blocked();
1646
+ this.commit.emit({ message: this.message().trim(), newBranch: toNew ? this.newBranch().trim() : undefined });
1647
+ }
1648
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitBox, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1649
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: CommitBox, isStandalone: true, selector: "o-commit-box", inputs: { repo: { classPropertyName: "repo", publicName: "repo", isSignal: true, isRequired: true, transformFunction: null }, branch: { classPropertyName: "branch", publicName: "branch", isSignal: true, isRequired: true, transformFunction: null }, defaultMessage: { classPropertyName: "defaultMessage", publicName: "defaultMessage", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { commit: "commit", cancel: "cancel" }, ngImport: i0, template: `
1650
+ <div class="o-col" style="gap:10px" data-testid="commit-box">
1651
+ @if (blocked()) {<div class="o-note o-blu">Your change is committed to a new branch with a pull request because <b>{{ branch() }}</b> is protected.</div>}
1652
+ <div class="o-fld"><label for="cb-msg">Commit message</label><input id="cb-msg" class="o-in" [ngModel]="message()" (ngModelChange)="message.set($event)" data-testid="commit-message" /></div>
1653
+ @if (!blocked()) {
1654
+ <label class="o-flex o-sm"><input type="radio" name="cb-where" [checked]="!toNew()" (change)="toNew.set(false)" /> Commit directly to <b class="o-mono">{{ branch() }}</b></label>
1655
+ <label class="o-flex o-sm"><input type="radio" name="cb-where" [checked]="toNew()" (change)="toNew.set(true)" data-testid="commit-new-branch" /> Create a new branch and open a pull request</label>
1656
+ }
1657
+ @if (toNew() || blocked()) {
1658
+ <div class="o-flex o-sm"><span class="o-faint">→ new branch</span><input class="o-in o-sm o-mono" style="max-width:260px" aria-label="New branch" [ngModel]="newBranch()" (ngModelChange)="newBranch.set($event)" data-testid="commit-branch" /></div>
1659
+ }
1660
+ <div class="o-flex">
1661
+ <button type="button" class="o-btn o-sm o-r" (click)="cancel.emit()">Cancel</button>
1662
+ <button type="button" class="o-btn o-sm o-pri" [disabled]="busy() || !message().trim() || ((toNew() || blocked()) && !newBranch().trim())" (click)="send()" data-testid="commit-submit">{{ toNew() || blocked() ? 'Commit and open PR' : 'Commit changes' }}</button>
1663
+ </div>
1664
+ </div>
1665
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1666
+ }
1667
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: CommitBox, decorators: [{
1668
+ type: Component,
1669
+ args: [{
1670
+ selector: 'o-commit-box',
1671
+ imports: [FormsModule],
1672
+ changeDetection: ChangeDetectionStrategy.OnPush,
1673
+ template: `
1674
+ <div class="o-col" style="gap:10px" data-testid="commit-box">
1675
+ @if (blocked()) {<div class="o-note o-blu">Your change is committed to a new branch with a pull request because <b>{{ branch() }}</b> is protected.</div>}
1676
+ <div class="o-fld"><label for="cb-msg">Commit message</label><input id="cb-msg" class="o-in" [ngModel]="message()" (ngModelChange)="message.set($event)" data-testid="commit-message" /></div>
1677
+ @if (!blocked()) {
1678
+ <label class="o-flex o-sm"><input type="radio" name="cb-where" [checked]="!toNew()" (change)="toNew.set(false)" /> Commit directly to <b class="o-mono">{{ branch() }}</b></label>
1679
+ <label class="o-flex o-sm"><input type="radio" name="cb-where" [checked]="toNew()" (change)="toNew.set(true)" data-testid="commit-new-branch" /> Create a new branch and open a pull request</label>
1680
+ }
1681
+ @if (toNew() || blocked()) {
1682
+ <div class="o-flex o-sm"><span class="o-faint">→ new branch</span><input class="o-in o-sm o-mono" style="max-width:260px" aria-label="New branch" [ngModel]="newBranch()" (ngModelChange)="newBranch.set($event)" data-testid="commit-branch" /></div>
1683
+ }
1684
+ <div class="o-flex">
1685
+ <button type="button" class="o-btn o-sm o-r" (click)="cancel.emit()">Cancel</button>
1686
+ <button type="button" class="o-btn o-sm o-pri" [disabled]="busy() || !message().trim() || ((toNew() || blocked()) && !newBranch().trim())" (click)="send()" data-testid="commit-submit">{{ toNew() || blocked() ? 'Commit and open PR' : 'Commit changes' }}</button>
1687
+ </div>
1688
+ </div>
1689
+ `,
1690
+ }]
1691
+ }], ctorParameters: () => [], propDecorators: { repo: [{ type: i0.Input, args: [{ isSignal: true, alias: "repo", required: true }] }], branch: [{ type: i0.Input, args: [{ isSignal: true, alias: "branch", required: true }] }], defaultMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultMessage", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], commit: [{ type: i0.Output, args: ["commit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
1692
+
1560
1693
  // /{org}/{project}/code/{repo}/blob?ref=&path= (prototype "code_file"): one file, highlighted, with
1561
1694
  // Markdown preview, blame, raw, copy and download; a line number links to that line (#L12).
1562
1695
  class FilePage extends RepoPage {
@@ -1583,6 +1716,17 @@ class FilePage extends RepoPage {
1583
1716
  return '';
1584
1717
  return [b.binary ? '' : `${b.lineCount} lines`, size(b.size), b.language ?? ''].filter(Boolean).join(' · ');
1585
1718
  }, ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
1719
+ this.mode = signal('', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
1720
+ this.text = signal(null, ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
1721
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
1722
+ this.branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : /* istanbul ignore next */ []));
1723
+ /** The branch the view is on ('' for a tag or a commit, which cannot take commits). */
1724
+ this.branch = computed(() => {
1725
+ const r = this.repo();
1726
+ const ref = this.ref() || r?.defaultBranch || '';
1727
+ return this.branches().includes(ref) ? ref : '';
1728
+ }, ...(ngDevMode ? [{ debugName: "branch" }] : /* istanbul ignore next */ []));
1729
+ this.editable = computed(() => this.canWrite() && !!this.branch(), ...(ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []));
1586
1730
  this.route.queryParamMap.subscribe((q) => {
1587
1731
  this.ref.set(q.get('ref') ?? '');
1588
1732
  this.path.set(q.get('path') ?? '');
@@ -1594,6 +1738,60 @@ class FilePage extends RepoPage {
1594
1738
  }
1595
1739
  onRepo(r) {
1596
1740
  this.load(r);
1741
+ this.api.refs(this.projectId(), r.name, 'branch').subscribe({ next: (b) => this.branches.set(b.map((x) => x.name)), error: () => undefined });
1742
+ }
1743
+ startEdit() {
1744
+ const r = this.repo();
1745
+ if (!r)
1746
+ return;
1747
+ this.mode.set('edit');
1748
+ this.text.set(null);
1749
+ this.api.blob(this.projectId(), r.name, this.branch(), this.path(), true).subscribe({
1750
+ next: (b) => {
1751
+ if (b.text === undefined) {
1752
+ this.mode.set('');
1753
+ this.error.set('This file cannot be edited in the browser.');
1754
+ return;
1755
+ }
1756
+ this.blob.set(b);
1757
+ this.text.set(b.text);
1758
+ },
1759
+ error: (e) => { this.mode.set(''); this.error.set(apiMessage(e, 'Could not open the file.')); },
1760
+ });
1761
+ }
1762
+ /** Commits the edit (or the delete), onto the branch or onto a new branch with a pull request. */
1763
+ save(c, del = false) {
1764
+ const r = this.repo();
1765
+ const b = this.blob();
1766
+ if (!r || !b)
1767
+ return;
1768
+ const body = c.newBranch
1769
+ ? { branch: c.newBranch, from: this.branch(), message: c.message, pull: {} }
1770
+ : { branch: this.branch(), expect: b.commit, message: c.message };
1771
+ if (del)
1772
+ body.deletes = [b.path];
1773
+ else
1774
+ body.files = { [b.path]: this.text() ?? '' };
1775
+ this.busy.set(true);
1776
+ this.error.set('');
1777
+ this.api.commitFiles(this.projectId(), r.name, body).subscribe({
1778
+ next: (res) => {
1779
+ this.busy.set(false);
1780
+ this.mode.set('');
1781
+ if (res.pull) {
1782
+ void this.router.navigate(this.link('pull/' + res.pull));
1783
+ return;
1784
+ }
1785
+ if (del) {
1786
+ void this.router.navigate(this.link(), { queryParams: { ref: res.branch, path: b.path.split('/').slice(0, -1).join('/') || null } });
1787
+ return;
1788
+ }
1789
+ this.note.set(`Committed ${res.commit.short} to ${res.branch}.`);
1790
+ void this.router.navigate(this.link('blob'), { queryParams: { ref: res.branch, path: b.path } });
1791
+ this.load(r);
1792
+ },
1793
+ error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not commit.')); },
1794
+ });
1597
1795
  }
1598
1796
  pickRef(ref) {
1599
1797
  void this.router.navigate(this.link('blob'), { queryParams: { ref, path: this.path() } });
@@ -1651,6 +1849,7 @@ class FilePage extends RepoPage {
1651
1849
  </nav>
1652
1850
  </div>
1653
1851
  @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
1852
+ @if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
1654
1853
  @if (blob(); as b) {
1655
1854
  <div class="o-card">
1656
1855
  @if (b.lastCommit; as lc) {
@@ -1670,8 +1869,26 @@ class FilePage extends RepoPage {
1670
1869
  @if (b.rawUrl) {<a class="o-btn o-xs" [href]="b.rawUrl" target="_blank" rel="noopener">Raw</a>}
1671
1870
  @if (!b.binary) {<button type="button" class="o-btn o-xs" (click)="copyText()">{{ copied() ? 'Copied' : 'Copy' }}</button>}
1672
1871
  @if (b.downloadUrl) {<a class="o-btn o-xs" [href]="b.downloadUrl">Download</a>}
1872
+ @if (editable()) {
1873
+ @if (!b.binary && !b.truncated) {<button type="button" class="o-btn o-xs" (click)="startEdit()" [disabled]="mode() !== ''" data-testid="file-edit">Edit</button>}
1874
+ <button type="button" class="o-btn o-xs" (click)="mode.set('delete')" [disabled]="mode() !== ''" data-testid="file-delete">Delete</button>
1875
+ }
1673
1876
  </span>
1674
1877
  </div>
1878
+ @if (mode() === 'edit') {
1879
+ <div class="o-cb o-col" style="gap:10px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft)">
1880
+ @if (text() === null) {<span class="o-faint o-sm">Loading the file…</span>} @else {
1881
+ <textarea class="o-in o-mono" rows="16" style="font-size:12px;height:auto;padding:8px 10px" aria-label="File contents" [ngModel]="text()" (ngModelChange)="text.set($event)" data-testid="file-text"></textarea>
1882
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="'Update ' + b.name" [busy]="busy()" (commit)="save($event)" (cancel)="mode.set('')" />
1883
+ }
1884
+ </div>
1885
+ }
1886
+ @if (mode() === 'delete') {
1887
+ <div class="o-cb o-col" style="gap:10px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft)">
1888
+ <span class="o-sm">Delete <b class="o-mono">{{ b.path }}</b> from <b class="o-mono">{{ branch() }}</b>?</span>
1889
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="'Delete ' + b.name" [busy]="busy()" (commit)="save($event, true)" (cancel)="mode.set('')" />
1890
+ </div>
1891
+ }
1675
1892
  @if (b.image && b.rawUrl) {
1676
1893
  <div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
1677
1894
  } @else if (b.binary || b.truncated) {
@@ -1698,11 +1915,11 @@ class FilePage extends RepoPage {
1698
1915
  <div class="o-card o-empty"><p>{{ error() }}</p></div>
1699
1916
  }
1700
1917
  </div>
1701
- `, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{text-decoration:none}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1918
+ `, isInline: true, styles: [":host ::ng-deep [class^=tk-k]{color:var(--o-tk-k)}:host ::ng-deep [class^=tk-s],:host ::ng-deep [class^=tk-l]{color:var(--o-tk-s)}:host ::ng-deep [class^=tk-c]{color:var(--o-tk-c);font-style:italic}:host ::ng-deep [class^=tk-m]{color:var(--o-tk-n)}:host ::ng-deep .tk-nf,:host ::ng-deep .tk-nx,:host ::ng-deep .tk-fm{color:var(--o-tk-f)}:host ::ng-deep .tk-nb,:host ::ng-deep .tk-nt,:host ::ng-deep .tk-bp{color:var(--o-tk-p)}:host ::ng-deep .tk-gi{color:var(--o-add)}:host ::ng-deep .tk-gd{color:var(--o-bad-tx)}\n", ".o-ln .o-n{text-decoration:none}.o-btn.o-on{background:var(--o-tx);border-color:var(--o-tx);color:var(--o-acc-fg)}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: RefPicker, selector: "o-ref-picker", inputs: ["repo", "current"], outputs: ["picked"] }, { 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: "component", type: CommitBox, selector: "o-commit-box", inputs: ["repo", "branch", "defaultMessage", "busy"], outputs: ["commit", "cancel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1702
1919
  }
1703
1920
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: FilePage, decorators: [{
1704
1921
  type: Component,
1705
- args: [{ selector: 'o-code-file', imports: [RouterLink, RefPicker], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1922
+ args: [{ selector: 'o-code-file', imports: [RouterLink, RefPicker, FormsModule, CommitBox], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1706
1923
  <div class="o-page o-wide">
1707
1924
  @if (repo(); as r) {
1708
1925
  <div class="o-flex o-wrap">
@@ -1716,6 +1933,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1716
1933
  </nav>
1717
1934
  </div>
1718
1935
  @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
1936
+ @if (note()) {<div class="o-note" role="status">{{ note() }}</div>}
1719
1937
  @if (blob(); as b) {
1720
1938
  <div class="o-card">
1721
1939
  @if (b.lastCommit; as lc) {
@@ -1735,8 +1953,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1735
1953
  @if (b.rawUrl) {<a class="o-btn o-xs" [href]="b.rawUrl" target="_blank" rel="noopener">Raw</a>}
1736
1954
  @if (!b.binary) {<button type="button" class="o-btn o-xs" (click)="copyText()">{{ copied() ? 'Copied' : 'Copy' }}</button>}
1737
1955
  @if (b.downloadUrl) {<a class="o-btn o-xs" [href]="b.downloadUrl">Download</a>}
1956
+ @if (editable()) {
1957
+ @if (!b.binary && !b.truncated) {<button type="button" class="o-btn o-xs" (click)="startEdit()" [disabled]="mode() !== ''" data-testid="file-edit">Edit</button>}
1958
+ <button type="button" class="o-btn o-xs" (click)="mode.set('delete')" [disabled]="mode() !== ''" data-testid="file-delete">Delete</button>
1959
+ }
1738
1960
  </span>
1739
1961
  </div>
1962
+ @if (mode() === 'edit') {
1963
+ <div class="o-cb o-col" style="gap:10px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft)">
1964
+ @if (text() === null) {<span class="o-faint o-sm">Loading the file…</span>} @else {
1965
+ <textarea class="o-in o-mono" rows="16" style="font-size:12px;height:auto;padding:8px 10px" aria-label="File contents" [ngModel]="text()" (ngModelChange)="text.set($event)" data-testid="file-text"></textarea>
1966
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="'Update ' + b.name" [busy]="busy()" (commit)="save($event)" (cancel)="mode.set('')" />
1967
+ }
1968
+ </div>
1969
+ }
1970
+ @if (mode() === 'delete') {
1971
+ <div class="o-cb o-col" style="gap:10px;background:var(--o-well);border-bottom:1px solid var(--o-line-soft)">
1972
+ <span class="o-sm">Delete <b class="o-mono">{{ b.path }}</b> from <b class="o-mono">{{ branch() }}</b>?</span>
1973
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="'Delete ' + b.name" [busy]="busy()" (commit)="save($event, true)" (cancel)="mode.set('')" />
1974
+ </div>
1975
+ }
1740
1976
  @if (b.image && b.rawUrl) {
1741
1977
  <div class="o-cb" style="text-align:center"><img [src]="b.rawUrl" [alt]="b.name" style="max-width:100%" /></div>
1742
1978
  } @else if (b.binary || b.truncated) {
@@ -3790,6 +4026,169 @@ function sortTags(tags) {
3790
4026
  return [...tags].sort((a, b) => when(b) - when(a) || b.name.localeCompare(a.name, undefined, { numeric: true }));
3791
4027
  }
3792
4028
 
4029
+ /** Uploads in one commit stay under the server's 25 MB request limit (base64 grows them by a third). */
4030
+ const MAX_UPLOAD = 18 * 1024 * 1024;
4031
+ // /{org}/{project}/code/{repo}/new?ref=&path= and /upload (DEV-13): a new file written in the
4032
+ // browser, or files uploaded from the computer, committed to the branch or to a new branch with a
4033
+ // pull request (protection applies).
4034
+ class NewFilePage extends RepoPage {
4035
+ constructor() {
4036
+ super();
4037
+ this.router = inject(Router);
4038
+ this.upload = signal(false, ...(ngDevMode ? [{ debugName: "upload" }] : /* istanbul ignore next */ []));
4039
+ this.ref = signal('', ...(ngDevMode ? [{ debugName: "ref" }] : /* istanbul ignore next */ []));
4040
+ this.uploads = signal([], ...(ngDevMode ? [{ debugName: "uploads" }] : /* istanbul ignore next */ []));
4041
+ this.busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
4042
+ this.filePath = '';
4043
+ this.content = '';
4044
+ this.dir = '';
4045
+ this.branch = computed(() => this.ref() || this.repo()?.defaultBranch || 'main', ...(ngDevMode ? [{ debugName: "branch" }] : /* istanbul ignore next */ []));
4046
+ this.defaultMessage = computed(() => (this.upload() ? 'Add files via upload' : 'Create a file'), ...(ngDevMode ? [{ debugName: "defaultMessage" }] : /* istanbul ignore next */ []));
4047
+ this.fmt = size;
4048
+ this.upload.set(this.route.snapshot.url.at(-1)?.path === 'upload');
4049
+ this.route.queryParamMap.subscribe((q) => {
4050
+ this.ref.set(q.get('ref') ?? '');
4051
+ const p = q.get('path') ?? '';
4052
+ this.dir = p;
4053
+ this.filePath = p ? p.replace(/\/+$/, '') + '/' : '';
4054
+ });
4055
+ }
4056
+ onRepo(_) { }
4057
+ async pick(ev) {
4058
+ const files = Array.from(ev.target.files ?? []);
4059
+ const out = [...this.uploads()];
4060
+ for (const f of files) {
4061
+ const buf = new Uint8Array(await f.arrayBuffer());
4062
+ let bin = '';
4063
+ for (let i = 0; i < buf.length; i += 0x8000)
4064
+ bin += String.fromCharCode(...buf.subarray(i, i + 0x8000));
4065
+ out.push({ name: f.name, size: f.size, b64: btoa(bin) });
4066
+ }
4067
+ if (out.reduce((a, u) => a + u.size, 0) > MAX_UPLOAD) {
4068
+ this.error.set('One upload is at most 18 MB; commit large files with Git.');
4069
+ return;
4070
+ }
4071
+ this.error.set('');
4072
+ this.uploads.set(out);
4073
+ }
4074
+ shown(name) {
4075
+ const prefix = this.dir.trim().replace(/^\/+|\/+$/g, '');
4076
+ return (prefix ? prefix + '/' : '') + name;
4077
+ }
4078
+ drop(name) {
4079
+ this.uploads.set(this.uploads().filter((u) => u.name !== name));
4080
+ }
4081
+ back() {
4082
+ void this.router.navigate(this.link(), { queryParams: { ref: this.ref() || null, path: this.dir || null } });
4083
+ }
4084
+ save(c) {
4085
+ const r = this.repo();
4086
+ if (!r)
4087
+ return;
4088
+ const body = c.newBranch ? { branch: c.newBranch, from: this.branch(), message: c.message, pull: {} } : { branch: this.branch(), message: c.message };
4089
+ if (this.upload()) {
4090
+ if (!this.uploads().length) {
4091
+ this.error.set('Choose files to upload.');
4092
+ return;
4093
+ }
4094
+ const prefix = this.dir.trim().replace(/^\/+|\/+$/g, '');
4095
+ body.uploads = Object.fromEntries(this.uploads().map((u) => [(prefix ? prefix + '/' : '') + u.name, u.b64]));
4096
+ }
4097
+ else {
4098
+ const p = this.filePath.trim().replace(/^\/+/, '');
4099
+ if (!p || p.endsWith('/')) {
4100
+ this.error.set('Name the file.');
4101
+ return;
4102
+ }
4103
+ body.files = { [p]: this.content };
4104
+ }
4105
+ this.busy.set(true);
4106
+ this.error.set('');
4107
+ this.api.commitFiles(this.projectId(), r.name, body).subscribe({
4108
+ next: (res) => {
4109
+ this.busy.set(false);
4110
+ if (res.pull) {
4111
+ void this.router.navigate(this.link('pull/' + res.pull));
4112
+ return;
4113
+ }
4114
+ if (!this.upload()) {
4115
+ void this.router.navigate(this.link('blob'), { queryParams: { ref: res.branch, path: this.filePath.trim().replace(/^\/+/, '') } });
4116
+ return;
4117
+ }
4118
+ void this.router.navigate(this.link(), { queryParams: { ref: res.branch, path: this.dir.trim() || null } });
4119
+ },
4120
+ error: (e) => { this.busy.set(false); this.error.set(apiMessage(e, 'Could not commit.')); },
4121
+ });
4122
+ }
4123
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewFilePage, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4124
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: NewFilePage, isStandalone: true, selector: "o-code-new-file", usesInheritance: true, ngImport: i0, template: `
4125
+ <div class="o-page o-wide">
4126
+ @if (repo(); as r) {
4127
+ <div class="o-ph"><div class="o-col"><span class="o-eyebrow">{{ r.name }} · {{ branch() }}</span><h1 class="o-t">{{ upload() ? 'Upload files' : 'Create a file' }}</h1></div></div>
4128
+ @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
4129
+ @if (!canWrite()) {
4130
+ <div class="o-card"><div class="o-empty">Your role cannot change this repository.</div></div>
4131
+ } @else {
4132
+ <div class="o-card">
4133
+ <div class="o-cb o-col" style="gap:12px">
4134
+ @if (upload()) {
4135
+ <div class="o-fld"><label for="nf-dir">Into folder</label><input id="nf-dir" class="o-in o-mono" [(ngModel)]="dir" placeholder="(the repository root)" /></div>
4136
+ <div class="o-fld"><label for="nf-files">Files</label><input id="nf-files" type="file" multiple class="o-in" (change)="pick($event)" data-testid="upload-files" /></div>
4137
+ @for (u of uploads(); track u.name) {<div class="o-row o-sm"><span class="o-mono">{{ shown(u.name) }}</span><span class="o-r o-faint">{{ fmt(u.size) }}</span><button type="button" class="o-lnk" (click)="drop(u.name)" [attr.aria-label]="'Remove ' + u.name">×</button></div>}
4138
+ } @else {
4139
+ <div class="o-fld"><label for="nf-path">File path</label><input id="nf-path" class="o-in o-mono" [(ngModel)]="filePath" placeholder="docs/notes.md" data-testid="new-file-path" /></div>
4140
+ <textarea class="o-in o-mono" rows="16" style="font-size:12px;height:auto;padding:8px 10px" aria-label="File contents" [(ngModel)]="content" data-testid="new-file-text"></textarea>
4141
+ }
4142
+ </div>
4143
+ <div class="o-cb" style="border-top:1px solid var(--o-line-soft)">
4144
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="defaultMessage()" [busy]="busy()" (commit)="save($event)" (cancel)="back()" />
4145
+ </div>
4146
+ </div>
4147
+ }
4148
+ } @else if (error()) {
4149
+ <div class="o-card o-empty"><p>{{ error() }}</p><a class="o-btn" [routerLink]="host.link('code')">All repositories</a></div>
4150
+ }
4151
+ </div>
4152
+ `, isInline: true, 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: CommitBox, selector: "o-commit-box", inputs: ["repo", "branch", "defaultMessage", "busy"], outputs: ["commit", "cancel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4153
+ }
4154
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: NewFilePage, decorators: [{
4155
+ type: Component,
4156
+ args: [{
4157
+ selector: 'o-code-new-file',
4158
+ imports: [FormsModule, RouterLink, CommitBox],
4159
+ changeDetection: ChangeDetectionStrategy.OnPush,
4160
+ template: `
4161
+ <div class="o-page o-wide">
4162
+ @if (repo(); as r) {
4163
+ <div class="o-ph"><div class="o-col"><span class="o-eyebrow">{{ r.name }} · {{ branch() }}</span><h1 class="o-t">{{ upload() ? 'Upload files' : 'Create a file' }}</h1></div></div>
4164
+ @if (error()) {<div class="o-note o-red" role="alert">{{ error() }}</div>}
4165
+ @if (!canWrite()) {
4166
+ <div class="o-card"><div class="o-empty">Your role cannot change this repository.</div></div>
4167
+ } @else {
4168
+ <div class="o-card">
4169
+ <div class="o-cb o-col" style="gap:12px">
4170
+ @if (upload()) {
4171
+ <div class="o-fld"><label for="nf-dir">Into folder</label><input id="nf-dir" class="o-in o-mono" [(ngModel)]="dir" placeholder="(the repository root)" /></div>
4172
+ <div class="o-fld"><label for="nf-files">Files</label><input id="nf-files" type="file" multiple class="o-in" (change)="pick($event)" data-testid="upload-files" /></div>
4173
+ @for (u of uploads(); track u.name) {<div class="o-row o-sm"><span class="o-mono">{{ shown(u.name) }}</span><span class="o-r o-faint">{{ fmt(u.size) }}</span><button type="button" class="o-lnk" (click)="drop(u.name)" [attr.aria-label]="'Remove ' + u.name">×</button></div>}
4174
+ } @else {
4175
+ <div class="o-fld"><label for="nf-path">File path</label><input id="nf-path" class="o-in o-mono" [(ngModel)]="filePath" placeholder="docs/notes.md" data-testid="new-file-path" /></div>
4176
+ <textarea class="o-in o-mono" rows="16" style="font-size:12px;height:auto;padding:8px 10px" aria-label="File contents" [(ngModel)]="content" data-testid="new-file-text"></textarea>
4177
+ }
4178
+ </div>
4179
+ <div class="o-cb" style="border-top:1px solid var(--o-line-soft)">
4180
+ <o-commit-box [repo]="r.name" [branch]="branch()" [defaultMessage]="defaultMessage()" [busy]="busy()" (commit)="save($event)" (cancel)="back()" />
4181
+ </div>
4182
+ </div>
4183
+ }
4184
+ } @else if (error()) {
4185
+ <div class="o-card o-empty"><p>{{ error() }}</p><a class="o-btn" [routerLink]="host.link('code')">All repositories</a></div>
4186
+ }
4187
+ </div>
4188
+ `,
4189
+ }]
4190
+ }], ctorParameters: () => [] });
4191
+
3793
4192
  /*
3794
4193
  * @chisquare-tech/origine-ngx-apps-code: the Code module's screens (TRD §6.10). The host app provides
3795
4194
  * CodeHost and mounts the pages at /{org}/{project}/code, /code/new, /code/pulls and
@@ -3800,5 +4199,5 @@ function sortTags(tags) {
3800
4199
  * Generated bundle index. Do not edit.
3801
4200
  */
3802
4201
 
3803
- 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 };
4202
+ export { BranchesPage, CodeApi, CodeHost, CommitBox, CommitPage, CommitsPage, CurrentRepo, CurrentRepoPage, DiffView, FilePage, NewFilePage, NewRepoPage, People, PullPage, PullsPage, RefPicker, ReleasesPage, RepoHomePage, RepoPage, RepoSettingsPage, ReposPage, TOKEN_CSS, ago, apiMessage, checkDot, copy, dayLabel, initials, langColour, matchesPattern, pair, shortDate, size };
3804
4203
  //# sourceMappingURL=chisquare-tech-origine-ngx-apps-code.mjs.map