From 57d90adec4fb3292dc60477f8e1527e61624e10e Mon Sep 17 00:00:00 2001 From: avwchapman Date: Fri, 14 Aug 2026 10:36:35 -0400 Subject: [PATCH 1/4] feat(release-tracks): add bulk and individual candidate review dialogs --- src/app/app.module.ts | 2 + .../release-review-dialog.component.html | 127 ++++++++++++ .../release-review-dialog.component.scss | 86 +++++++++ .../release-review-dialog.component.spec.ts | 58 ++++++ .../release-review-dialog.component.ts | 109 +++++++++++ .../release-track-page.component.html | 9 + .../release-track-page.component.spec.ts | 83 +++++++- .../release-track-page.component.ts | 180 +++++++++++++++--- 8 files changed, 627 insertions(+), 27 deletions(-) create mode 100644 src/app/components/release-review-dialog/release-review-dialog.component.html create mode 100644 src/app/components/release-review-dialog/release-review-dialog.component.scss create mode 100644 src/app/components/release-review-dialog/release-review-dialog.component.spec.ts create mode 100644 src/app/components/release-review-dialog/release-review-dialog.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4ba43f17..480b5f31 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -151,6 +151,7 @@ import { IconViewComponent } from './components/icon-view/icon-view.component'; import { ObjectStatusComponent } from './components/object-status/object-status.component'; import { RecentActivityComponent } from './components/recent-activity/recent-activity.component'; import { NotesEditorComponent } from './components/stix/stix-page-tabs/notes-editor/notes-editor.component'; +import { ReleaseReviewDialogComponent } from './components/release-review-dialog/release-review-dialog.component'; import { IdentityPropertyComponent } from './components/stix/identity-property/identity-property.component'; import { CitationEditComponent } from './components/stix/citation-property/citation-edit/citation-edit.component'; @@ -350,6 +351,7 @@ export function initConfig(appConfigService: AppConfigService) { OrderedListViewComponent, OrderedListEditComponent, NotesEditorComponent, + ReleaseReviewDialogComponent, ObjectStatusComponent, RecentActivityComponent, IdentityPropertyComponent, diff --git a/src/app/components/release-review-dialog/release-review-dialog.component.html b/src/app/components/release-review-dialog/release-review-dialog.component.html new file mode 100644 index 00000000..c57cb9a3 --- /dev/null +++ b/src/app/components/release-review-dialog/release-review-dialog.component.html @@ -0,0 +1,127 @@ + diff --git a/src/app/components/release-review-dialog/release-review-dialog.component.scss b/src/app/components/release-review-dialog/release-review-dialog.component.scss new file mode 100644 index 00000000..36f1ca24 --- /dev/null +++ b/src/app/components/release-review-dialog/release-review-dialog.component.scss @@ -0,0 +1,86 @@ +@use '../../../style/colors'; + +.release-review-dialog-panel .mat-mdc-dialog-surface { + border: 1px solid; + border-radius: 10px; + overflow: hidden; + @include colors.theme-border-color; +} + +.release-review-dialog { + display: flex; + width: min(94vw, 1160px); + max-height: min(90vh, 900px); + flex-direction: column; + + .release-review-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + padding: 18px 24px; + border-bottom: 1px solid; + @include colors.theme-border-color; + @include colors.theme-property( + background, + rgba(colors.on-color(dark), 0.045), + rgba(colors.color(mitre-black), 0.025) + ); + + h2, + p { + margin: 0; + } + + p { + margin-top: 4px; + @include colors.theme-text-deemphasis; + + span { + margin-left: 12px; + font-weight: 700; + } + } + } + + .release-review-content { + min-height: 0; + flex: 1 1 auto; + padding: 20px 24px; + overflow: auto; + } + + .release-review-note { + padding: 16px 24px 0; + border-top: 1px solid; + @include colors.theme-border-color; + + mat-form-field { + width: 100%; + } + } + + .release-review-actions { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid; + @include colors.theme-border-color; + + .action-spacer { + flex: 1 1 auto; + } + } +} + +@media (max-width: 700px) { + .release-review-dialog .release-review-actions { + align-items: stretch; + flex-direction: column; + + .action-spacer { + display: none; + } + } +} diff --git a/src/app/components/release-review-dialog/release-review-dialog.component.spec.ts b/src/app/components/release-review-dialog/release-review-dialog.component.spec.ts new file mode 100644 index 00000000..096d0947 --- /dev/null +++ b/src/app/components/release-review-dialog/release-review-dialog.component.spec.ts @@ -0,0 +1,58 @@ +import { ReleaseReviewDialogComponent } from './release-review-dialog.component'; + +describe('ReleaseReviewDialogComponent', () => { + const createComponent = (count = 2) => { + const dialogRef = { close: vi.fn() } as any; + const items = Array.from({ length: count }, (_, index) => ({ + item: { + object_ref: `attack-pattern--${index}`, + name: `Technique ${index}`, + }, + current: { + name: `Technique ${index}`, + type: 'attack-pattern', + }, + prior: null, + })) as any; + return { + component: new ReleaseReviewDialogComponent(dialogRef, { items }), + dialogRef, + }; + }; + + it('steps through items and returns approved objects', () => { + const { component, dialogRef } = createComponent(); + + component.approve(); + + expect(component.index).toBe(1); + expect(dialogRef.close).not.toHaveBeenCalled(); + + component.skip(); + + expect(dialogRef.close).toHaveBeenCalledWith({ + approved: [expect.objectContaining({ object_ref: 'attack-pattern--0' })], + updateRequests: [], + }); + }); + + it('requires a note before requesting updates', () => { + const { component, dialogRef } = createComponent(1); + + component.requestUpdates(); + expect(dialogRef.close).not.toHaveBeenCalled(); + + component.note = 'Please update the description.'; + component.requestUpdates(); + + expect(dialogRef.close).toHaveBeenCalledWith({ + approved: [], + updateRequests: [ + { + item: expect.objectContaining({ object_ref: 'attack-pattern--0' }), + note: 'Please update the description.', + }, + ], + }); + }); +}); diff --git a/src/app/components/release-review-dialog/release-review-dialog.component.ts b/src/app/components/release-review-dialog/release-review-dialog.component.ts new file mode 100644 index 00000000..242ba5df --- /dev/null +++ b/src/app/components/release-review-dialog/release-review-dialog.component.ts @@ -0,0 +1,109 @@ +import { Component, Inject, ViewEncapsulation } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +import { StixObject } from 'src/app/classes/stix'; +import { ReleaseTrackObjectItem } from 'src/app/components/release-track-object-card/release-track-object-card.component'; +import { StixViewConfig } from 'src/app/views/stix/stix-view-page'; + +export interface ReleaseReviewItem { + item: ReleaseTrackObjectItem; + current: StixObject; + prior: StixObject | null; +} + +export interface ReleaseReviewDialogData { + items: ReleaseReviewItem[]; +} + +export interface ReleaseReviewUpdateRequest { + item: ReleaseTrackObjectItem; + note: string; +} + +export interface ReleaseReviewDialogResult { + approved: ReleaseTrackObjectItem[]; + updateRequests: ReleaseReviewUpdateRequest[]; +} + +@Component({ + selector: 'app-release-review-dialog', + templateUrl: './release-review-dialog.component.html', + styleUrls: ['./release-review-dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + standalone: false, +}) +export class ReleaseReviewDialogComponent { + public index = 0; + public note = ''; + public showNote = false; + + private readonly result: ReleaseReviewDialogResult = { + approved: [], + updateRequests: [], + }; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ReleaseReviewDialogData + ) {} + + public get reviewItem(): ReleaseReviewItem { + return this.data.items[this.index]; + } + + public get objectName(): string { + return ( + this.reviewItem?.item?.name || + this.reviewItem?.item?.attack_id || + 'Object' + ); + } + + public get progressLabel(): string { + return `${this.index + 1} of ${this.data.items.length}`; + } + + public get config(): StixViewConfig { + return { + mode: 'diff', + object: [this.reviewItem.current, this.reviewItem.prior], + editable: false, + sidebarControl: 'disable', + showRelationships: false, + }; + } + + public approve(): void { + this.result.approved.push(this.reviewItem.item); + this.next(); + } + + public requestUpdates(): void { + const note = this.note.trim(); + if (!note) return; + + this.result.updateRequests.push({ item: this.reviewItem.item, note }); + this.next(); + } + + public skip(): void { + this.next(); + } + + public cancel(): void { + const hasCompletedActions = + this.result.approved.length > 0 || this.result.updateRequests.length > 0; + this.dialogRef.close(hasCompletedActions ? this.result : undefined); + } + + private next(): void { + if (this.index === this.data.items.length - 1) { + this.dialogRef.close(this.result); + return; + } + + this.index += 1; + this.note = ''; + this.showNote = false; + } +} diff --git a/src/app/views/dashboard-page/release-management/release-track-page/release-track-page.component.html b/src/app/views/dashboard-page/release-management/release-track-page/release-track-page.component.html index a5c56555..81ce721d 100644 --- a/src/app/views/dashboard-page/release-management/release-track-page/release-track-page.component.html +++ b/src/app/views/dashboard-page/release-management/release-track-page/release-track-page.component.html @@ -335,6 +335,15 @@

Resolution Details

{{ lane.title }} ({{ lane.items.length }})
+ +