feat(ui): adopt domain signal chips on mounted surfaces [SPRINT-013]
Replace hand-rolled digest truncation/copy and reachability badges with shared DigestChipComponent and ReachabilityStateChipComponent on releases list, evidence-thread, attestation-links, and reachability-center. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -144,10 +144,9 @@ describe('EvidenceThreadViewComponent', () => {
|
||||
expect(component.getVerdictIcon('unknown')).toBe('help_outline');
|
||||
});
|
||||
|
||||
it('should compute short digest correctly', () => {
|
||||
it('should set artifact digest for the digest chip', () => {
|
||||
fixture.detectChanges();
|
||||
const shortDigest = component.shortDigest();
|
||||
expect(shortDigest).toBe('sha256:abc123...');
|
||||
expect(component.artifactDigest()).toBe('sha256:abc123');
|
||||
});
|
||||
|
||||
it('should compute node count correctly', () => {
|
||||
|
||||
@@ -92,7 +92,10 @@
|
||||
<td mat-cell *matCellDef="let thread">
|
||||
<div class="artifact-cell">
|
||||
<span class="artifact-name">{{ thread.artifactName ?? 'Unnamed' }}</span>
|
||||
<code class="artifact-digest">{{ shortDigest(thread.artifactDigest) }}</code>
|
||||
<app-digest-chip
|
||||
[digest]="thread.artifactDigest"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
EvidenceVerdict,
|
||||
EvidenceThreadFilter
|
||||
} from '../../services/evidence-thread.service';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-evidence-thread-list',
|
||||
@@ -44,7 +45,8 @@ import {
|
||||
MatChipsModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatTooltipModule,
|
||||
MatCardModule
|
||||
MatCardModule,
|
||||
DigestChipComponent
|
||||
],
|
||||
templateUrl: './evidence-thread-list.component.html',
|
||||
styleUrls: ['./evidence-thread-list.component.scss'],
|
||||
@@ -194,10 +196,6 @@ export class EvidenceThreadListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
shortDigest(digest: string): string {
|
||||
return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest;
|
||||
}
|
||||
|
||||
getRiskClass(riskScore: number): string {
|
||||
if (riskScore >= 7) return 'risk-critical';
|
||||
if (riskScore >= 4) return 'risk-high';
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
}
|
||||
</h1>
|
||||
<div class="thread-digest">
|
||||
<code>{{ shortDigest() }}</code>
|
||||
<button mat-icon-button (click)="copyDigest()" [matTooltip]="'ui.evidence_thread.copy_digest' | translate" class="copy-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
<app-digest-chip
|
||||
[digest]="artifactDigest()"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { EvidenceTranscriptPanelComponent } from '../evidence-transcript-panel/e
|
||||
import { EvidenceNodeCardComponent } from '../evidence-node-card/evidence-node-card.component';
|
||||
import { EvidenceExportDialogComponent } from '../evidence-export-dialog/evidence-export-dialog.component';
|
||||
import { TranslatePipe } from '../../../../core/i18n/translate.pipe';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
@Component({
|
||||
selector: 'stella-evidence-thread-view',
|
||||
@@ -42,7 +43,8 @@ import { TranslatePipe } from '../../../../core/i18n/translate.pipe';
|
||||
EvidenceTimelinePanelComponent,
|
||||
EvidenceTranscriptPanelComponent,
|
||||
EvidenceNodeCardComponent,
|
||||
TranslatePipe
|
||||
TranslatePipe,
|
||||
DigestChipComponent
|
||||
],
|
||||
templateUrl: './evidence-thread-view.component.html',
|
||||
styleUrls: ['./evidence-thread-view.component.scss'],
|
||||
@@ -91,13 +93,6 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy {
|
||||
readonly nodeCount = computed(() => this.nodes().length);
|
||||
readonly linkCount = computed(() => this.links().length);
|
||||
|
||||
readonly shortDigest = computed(() => {
|
||||
const digest = this.artifactDigest();
|
||||
if (!digest) return '';
|
||||
// Show first 12 chars of digest (sha256:xxxx...)
|
||||
return digest.length > 19 ? `${digest.substring(0, 19)}...` : digest;
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.pipe(takeUntil(this.destroy$)).subscribe(params => {
|
||||
const digest = params['artifactDigest'];
|
||||
@@ -182,23 +177,4 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy {
|
||||
return icons[verdict ?? 'unknown'] ?? 'help_outline';
|
||||
}
|
||||
|
||||
copyDigest(): void {
|
||||
const digest = this.artifactDigest();
|
||||
if (!digest || typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
||||
this.snackBar.open('Clipboard not available', 'Dismiss', {
|
||||
duration: 3000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(digest).then(() => {
|
||||
this.snackBar.open('Digest copied to clipboard', 'Dismiss', {
|
||||
duration: 2000
|
||||
});
|
||||
}).catch(() => {
|
||||
this.snackBar.open('Failed to copy digest', 'Dismiss', {
|
||||
duration: 3000
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Unit tests for AttestationLinksComponent -- DigestChipComponent adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { AttestationLinksComponent } from './attestation-links.component';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
describe('AttestationLinksComponent - DigestChip adoption', () => {
|
||||
let component: AttestationLinksComponent;
|
||||
let fixture: ComponentFixture<AttestationLinksComponent>;
|
||||
|
||||
const mockAttestations = [
|
||||
{
|
||||
digest: 'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
|
||||
predicateType: 'https://slsa.dev/provenance/v1',
|
||||
createdAt: '2026-01-15T10:30:00Z',
|
||||
rekorIndex: 12345,
|
||||
rekorLogId: 'abc123',
|
||||
viewUrl: 'https://example.com/att/1'
|
||||
},
|
||||
{
|
||||
digest: 'sha256:deadbeef0000111122223333444455556666777788889999aaaabbbbccccddd0',
|
||||
predicateType: 'https://in-toto.io/Statement/v1',
|
||||
createdAt: '2026-01-16T14:00:00Z',
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AttestationLinksComponent]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AttestationLinksComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.attestations = mockAttestations;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render one DigestChipComponent per attestation', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
expect(digestChips.length).toBe(mockAttestations.length);
|
||||
});
|
||||
|
||||
it('should pass attestation digest to each chip', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
digestChips.forEach((chip, index) => {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.digest).toBe(mockAttestations[index].digest);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use artifact variant for digest chips', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.variant).toBe('artifact');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have hand-rolled truncateDigest method', () => {
|
||||
expect((component as any).truncateDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not have hand-rolled copyDigest method', () => {
|
||||
expect((component as any).copyDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should still render Rekor links when available', () => {
|
||||
const rekorLinks = fixture.debugElement.queryAll(By.css('.rekor-link'));
|
||||
expect(rekorLinks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -8,17 +8,17 @@ import { Component, Input } from '@angular/core';
|
||||
|
||||
import { AttestationLink } from '../../models/lineage.models';
|
||||
import {
|
||||
ICON_CLIPBOARD,
|
||||
ICON_EXTERNAL_LINK,
|
||||
ICON_CHECK,
|
||||
} from '../../icons/lineage-icons';
|
||||
import { DigestChipComponent } from '../../../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
/**
|
||||
* Attestation links component showing signed attestations with Rekor links.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-attestation-links',
|
||||
imports: [],
|
||||
imports: [DigestChipComponent],
|
||||
template: `
|
||||
<div class="attestation-links">
|
||||
@if (attestations.length === 0) {
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
<div class="card-body">
|
||||
<div class="digest-row">
|
||||
<span class="label">Digest:</span>
|
||||
<code class="digest">{{ truncateDigest(att.digest) }}</code>
|
||||
<button class="copy-btn" (click)="copyDigest(att.digest)" title="Copy full digest">
|
||||
<span [innerHTML]="clipboardIcon"></span>
|
||||
</button>
|
||||
<app-digest-chip
|
||||
[digest]="att.digest"
|
||||
variant="artifact"
|
||||
></app-digest-chip>
|
||||
</div>
|
||||
|
||||
@if (att.rekorIndex !== undefined) {
|
||||
@@ -195,7 +195,6 @@ import {
|
||||
`]
|
||||
})
|
||||
export class AttestationLinksComponent {
|
||||
readonly clipboardIcon = ICON_CLIPBOARD;
|
||||
readonly externalLinkIcon = ICON_EXTERNAL_LINK;
|
||||
readonly checkIcon = ICON_CHECK;
|
||||
|
||||
@@ -220,15 +219,6 @@ export class AttestationLinksComponent {
|
||||
}
|
||||
}
|
||||
|
||||
truncateDigest(digest: string): string {
|
||||
if (!digest) return '';
|
||||
const colonIndex = digest.indexOf(':');
|
||||
if (colonIndex >= 0 && digest.length > colonIndex + 16) {
|
||||
return `${digest.substring(0, colonIndex + 17)}...`;
|
||||
}
|
||||
return digest.length > 20 ? `${digest.substring(0, 20)}...` : digest;
|
||||
}
|
||||
|
||||
getRekorUrl(att: AttestationLink): string {
|
||||
if (att.rekorLogId) {
|
||||
return `https://search.sigstore.dev/?logIndex=${att.rekorIndex}`;
|
||||
@@ -239,11 +229,4 @@ export class AttestationLinksComponent {
|
||||
return '#';
|
||||
}
|
||||
|
||||
async copyDigest(digest: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(digest);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy digest:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Focused tests for ReachabilityCenterComponent -- ReachabilityStateChip adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReachabilityCenterComponent } from './reachability-center.component';
|
||||
import type { ReachabilityWitness } from '../../core/api/witness.models';
|
||||
|
||||
describe('ReachabilityCenterComponent - ReachabilityStateChip adoption', () => {
|
||||
let fixture: ComponentFixture<ReachabilityCenterComponent>;
|
||||
let component: ReachabilityCenterComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReachabilityCenterComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReachabilityCenterComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should map reachable witness to Reachable state', () => {
|
||||
const witness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
|
||||
expect(component.reachabilityState(witness)).toBe('Reachable');
|
||||
});
|
||||
|
||||
it('should map unreachable witness to Unreachable state', () => {
|
||||
const witness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
|
||||
expect(component.reachabilityState(witness)).toBe('Unreachable');
|
||||
});
|
||||
|
||||
it('should preserve existing reachabilityLabel for backward compat', () => {
|
||||
const reachableWitness = { isReachable: true, confidenceScore: 0.85 } as ReachabilityWitness;
|
||||
const unreachableWitness = { isReachable: false, confidenceScore: 0.92 } as ReachabilityWitness;
|
||||
|
||||
expect(component.reachabilityLabel(reachableWitness)).toBe('Reachable');
|
||||
expect(component.reachabilityLabel(unreachableWitness)).toBe('Unreachable');
|
||||
});
|
||||
|
||||
it('should preserve existing confidenceLabel for backward compat', () => {
|
||||
const witness = { isReachable: true, confidenceScore: 0.856 } as ReachabilityWitness;
|
||||
expect(component.confidenceLabel(witness)).toBe('86%');
|
||||
});
|
||||
|
||||
it('should round confidence to nearest integer percent', () => {
|
||||
const witness50 = { isReachable: true, confidenceScore: 0.5 } as ReachabilityWitness;
|
||||
const witness99 = { isReachable: false, confidenceScore: 0.999 } as ReachabilityWitness;
|
||||
const witness0 = { isReachable: false, confidenceScore: 0 } as ReachabilityWitness;
|
||||
|
||||
expect(component.confidenceLabel(witness50)).toBe('50%');
|
||||
expect(component.confidenceLabel(witness99)).toBe('100%');
|
||||
expect(component.confidenceLabel(witness0)).toBe('0%');
|
||||
});
|
||||
});
|
||||
@@ -172,7 +172,10 @@
|
||||
<tr data-testid="witness-row">
|
||||
<td>
|
||||
<strong>{{ witness.witnessId }}</strong>
|
||||
<span class="subtle">{{ reachabilityLabel(witness) }}</span>
|
||||
<app-reachability-state-chip
|
||||
[state]="reachabilityState(witness)"
|
||||
[confidence]="witness.confidenceScore"
|
||||
></app-reachability-state-chip>
|
||||
</td>
|
||||
<td>{{ witness.cveId ?? witness.vulnId }}</td>
|
||||
<td>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
readContextRouteState,
|
||||
} from '../../shared/ui/context-route-state/context-route-state';
|
||||
import { PoEDrawerComponent } from './poe-drawer.component';
|
||||
import { ReachabilityStateChipComponent, type ReachabilityState } from '../../shared/domain/reachability-state-chip/reachability-state-chip.component';
|
||||
import {
|
||||
type CoverageStatus,
|
||||
DEFAULT_REACHABILITY_SCAN_ID,
|
||||
@@ -71,6 +72,7 @@ const TIER_FILTERS: readonly TierFilter[] = [
|
||||
PoEDrawerComponent,
|
||||
ContextHeaderComponent,
|
||||
TabbedNavComponent,
|
||||
ReachabilityStateChipComponent,
|
||||
],
|
||||
templateUrl: './reachability-center.component.html',
|
||||
styleUrls: ['./reachability-center.component.scss'],
|
||||
@@ -358,6 +360,10 @@ export class ReachabilityCenterComponent implements OnInit {
|
||||
return witness.isReachable ? 'Reachable' : 'Unreachable';
|
||||
}
|
||||
|
||||
reachabilityState(witness: ReachabilityWitness): ReachabilityState {
|
||||
return witness.isReachable ? 'Reachable' : 'Unreachable';
|
||||
}
|
||||
|
||||
artifactRouteId(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Unit tests for ReleasesListPageComponent -- DigestChipComponent adoption.
|
||||
* Sprint: SPRINT_20260308_013_FE_orphan_domain_signal_chips_adoption (FE-ODSC-004)
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { ReleasesListPageComponent } from './releases-list-page.component';
|
||||
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
describe('ReleasesListPageComponent - DigestChip adoption', () => {
|
||||
let component: ReleasesListPageComponent;
|
||||
let fixture: ComponentFixture<ReleasesListPageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReleasesListPageComponent],
|
||||
providers: [provideRouter([])]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReleasesListPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render DigestChipComponent for each release row', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
// The component uses fixture data; verify at least one chip renders
|
||||
expect(digestChips.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should pass bundleDigest to each digest chip', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.digest).toBeTruthy();
|
||||
// Digests should contain sha256: prefix
|
||||
expect(chipInstance.digest).toContain('sha256:');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use bundle variant for digest chips', () => {
|
||||
const digestChips = fixture.debugElement.queryAll(
|
||||
By.directive(DigestChipComponent)
|
||||
);
|
||||
for (const chip of digestChips) {
|
||||
const chipInstance = chip.componentInstance as DigestChipComponent;
|
||||
expect(chipInstance.variant).toBe('bundle');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have hand-rolled shortDigest method', () => {
|
||||
// Verify the old method was removed
|
||||
expect((component as any).shortDigest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not have hand-rolled copyDigest method', () => {
|
||||
// Verify the old method was removed
|
||||
expect((component as any).copyDigest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/c
|
||||
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { DigestChipComponent } from '../../shared/domain/digest-chip/digest-chip.component';
|
||||
|
||||
interface Release {
|
||||
id: string;
|
||||
@@ -24,7 +25,7 @@ interface Release {
|
||||
@Component({
|
||||
selector: 'app-releases-list-page',
|
||||
standalone: true,
|
||||
imports: [RouterLink, FormsModule],
|
||||
imports: [RouterLink, FormsModule, DigestChipComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="releases-page">
|
||||
@@ -95,15 +96,10 @@ interface Release {
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code class="digest" [title]="release.bundleDigest">
|
||||
{{ shortDigest(release.bundleDigest) }}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
class="copy-btn"
|
||||
(click)="copyDigest(release.bundleDigest)"
|
||||
title="Copy digest"
|
||||
><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
||||
<app-digest-chip
|
||||
[digest]="release.bundleDigest"
|
||||
variant="bundle"
|
||||
></app-digest-chip>
|
||||
</td>
|
||||
<td>{{ release.components }}</td>
|
||||
<td>{{ release.environment }}</td>
|
||||
@@ -315,14 +311,6 @@ export class ReleasesListPageComponent {
|
||||
});
|
||||
});
|
||||
|
||||
shortDigest(digest: string): string {
|
||||
const parts = digest.split(':');
|
||||
if (parts.length === 2 && parts[1].length > 10) {
|
||||
return `${parts[0]}:${parts[1].substring(0, 4)}...${parts[1].substring(parts[1].length - 3)}`;
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
onSearch(query: string): void {
|
||||
this.searchQuery.set(query);
|
||||
}
|
||||
@@ -363,14 +351,6 @@ export class ReleasesListPageComponent {
|
||||
this.gateFilter.set(select.value);
|
||||
}
|
||||
|
||||
async copyDigest(digest: string): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(digest);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy digest:', err);
|
||||
}
|
||||
}
|
||||
|
||||
openCreateRelease(): void {
|
||||
console.log('Open create release modal');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user